├── .eslintrc ├── .github └── workflows │ ├── deploy.yml │ └── verify.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.config.ts ├── misc ├── code-examples │ └── react.md └── intro.jpg ├── package.json ├── playground ├── index.html ├── main.ts └── vite.config.ts ├── renovate.json ├── src ├── facile-validator.ts ├── index.ts ├── locales │ ├── cs.ts │ ├── de.ts │ ├── en.ts │ ├── fa.ts │ ├── fr.ts │ ├── index.ts │ ├── it.ts │ ├── nl.ts │ └── zh.ts ├── modules │ ├── events.ts │ ├── language.ts │ ├── rule-adapter.ts │ ├── rule-error.ts │ └── validator-error.ts ├── rules │ ├── accepted.ts │ ├── alpha-num-dash.ts │ ├── alpha-num.ts │ ├── alpha.ts │ ├── between.ts │ ├── digits.ts │ ├── email.ts │ ├── ends-with.ts │ ├── index.ts │ ├── int.ts │ ├── max.ts │ ├── min.ts │ ├── num-dash.ts │ ├── number.ts │ ├── regex.ts │ ├── required-if.ts │ ├── required.ts │ ├── size.ts │ ├── starts-with.ts │ └── within.ts ├── types │ ├── elements.ts │ ├── error-dev.ts │ ├── index.ts │ └── rules.ts └── utils │ ├── helpers.ts │ └── regex.ts ├── tests ├── rules │ ├── accepted.test.ts │ ├── alpha-num-dash.test.ts │ ├── alpha-num.test.ts │ ├── alpha.test.ts │ ├── between.test.ts │ ├── digits.test.ts │ ├── email.test.ts │ ├── ends-with.test.ts │ ├── int.test.ts │ ├── max.test.ts │ ├── min.test.ts │ ├── num-dash.test.ts │ ├── number.test.ts │ ├── regex.test.ts │ ├── required.test.ts │ ├── size.test.ts │ ├── starts-with.test.ts │ └── within.test.ts └── utils │ ├── process-args.test.ts │ ├── process-rule.test.ts │ └── to-camel-case.test.ts ├── tsconfig.json ├── vitest.config.ts └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:prettier/recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "rules": {} 19 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-and-deploy: 8 | concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v4 13 | 14 | - name: Install dependencies 15 | run: yarn 16 | 17 | - name: Generate Demo 18 | run: yarn deploy 19 | 20 | - name: Deploy 🚀 21 | uses: JamesIves/github-pages-deploy-action@v4.7.3 22 | with: 23 | branch: gh-pages # The branch the action should deploy to. 24 | folder: playground/dist # The folder the action should deploy. 25 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | verify: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | node: [18, 20] 19 | 20 | steps: 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | - name: checkout 26 | uses: actions/checkout@master 27 | 28 | - name: Install dependencies 29 | run: yarn 30 | 31 | - name: Lint 32 | run: yarn lint 33 | 34 | - name: Test 35 | run: yarn test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | coverage 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "semi": true, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.12.3](https://github.com/upjs/facile-validator/compare/v1.12.2...v1.12.3) (2023-11-28) 6 | 7 | ### [1.12.2](https://github.com/upjs/facile-validator/compare/v1.12.1...v1.12.2) (2023-11-28) 8 | 9 | ### [1.12.1](https://github.com/upjs/facile-validator/compare/v1.12.0...v1.12.1) (2023-11-28) 10 | 11 | ## [1.12.0](https://github.com/upjs/facile-validator/compare/v1.11.9...v1.12.0) (2023-11-28) 12 | 13 | ### Features 14 | 15 | - add possibility to use functions for custom error message ([025b42e](https://github.com/upjs/facile-validator/commit/025b42e30b8fbc4f201bf96e4a7c99516b63c0e5)) 16 | 17 | ### [1.11.9](https://github.com/upjs/facile-validator/compare/v1.11.8...v1.11.9) (2023-10-14) 18 | 19 | ### [1.11.8](https://github.com/upjs/facile-validator/compare/v1.11.7...v1.11.8) (2023-10-14) 20 | 21 | ### [1.11.7](https://github.com/upjs/facile-validator/compare/v1.11.6...v1.11.7) (2023-10-14) 22 | 23 | ### [1.11.6](https://github.com/upjs/facile-validator/compare/v1.11.5...v1.11.6) (2023-10-14) 24 | 25 | ### [1.11.5](https://github.com/upjs/facile-validator/compare/v1.11.4...v1.11.5) (2023-10-14) 26 | 27 | ### [1.11.4](https://github.com/upjs/facile-validator/compare/v1.11.3...v1.11.4) (2023-10-14) 28 | 29 | ### [1.11.3](https://github.com/upjs/facile-validator/compare/v1.11.2...v1.11.3) (2023-09-30) 30 | 31 | ### [1.11.2](https://github.com/upjs/facile-validator/compare/v1.11.1...v1.11.2) (2023-09-30) 32 | 33 | ### [1.11.1](https://github.com/upjs/facile-validator/compare/v1.11.0...v1.11.1) (2023-09-30) 34 | 35 | ## [1.11.0](https://github.com/upjs/facile-validator/compare/v1.10.0...v1.11.0) (2023-09-30) 36 | 37 | ### Features 38 | 39 | - add CS and NL languages ([bf88c1c](https://github.com/upjs/facile-validator/commit/bf88c1c01cbf3b34b02dc7ee25e52550e68df38e)) 40 | 41 | ## [1.10.0](https://github.com/upjs/facile-validator/compare/v1.9.0...v1.10.0) (2022-08-20) 42 | 43 | ### Features 44 | 45 | - add nl language ([18746ee](https://github.com/upjs/facile-validator/commit/18746eee9b1b907af8edd41edca9868793ad047c)) 46 | 47 | ## [1.9.0](https://github.com/upjs/facile-validator/compare/v1.8.0...v1.9.0) (2022-08-17) 48 | 49 | ### Features 50 | 51 | - add cs lang ([f171b46](https://github.com/upjs/facile-validator/commit/f171b4610de4d61c9952a3d79c6de7bd2e194a95)) 52 | 53 | ## [1.8.0](https://github.com/upjs/facile-validator/compare/v1.7.0...v1.8.0) (2022-08-11) 54 | 55 | ### Features 56 | 57 | - Add new locales. ([c60add0](https://github.com/upjs/facile-validator/commit/c60add0c22cacba4228a72129f093e6d5d6e913e)) 58 | 59 | ## [1.7.0](https://github.com/upjs/facile-validator/compare/v1.6.0...v1.7.0) (2022-08-09) 60 | 61 | ### Features 62 | 63 | - change language on runtime ([ee25529](https://github.com/upjs/facile-validator/commit/ee2552948e895ffe015e48522a44b9203cbd1997)) 64 | 65 | ## [1.6.0](https://github.com/upjs/facile-validator/compare/v1.4.0...v1.6.0) (2022-08-09) 66 | 67 | ### Features 68 | 69 | - add support for textarea validation ([76482eb](https://github.com/upjs/facile-validator/commit/76482eb8fe4dab0372b27a82f6765277eaec93fb)) 70 | 71 | ### Bug Fixes 72 | 73 | - use `unknown` instead of `any` ([41baeb5](https://github.com/upjs/facile-validator/commit/41baeb505a32c3b09b02c133080a5bdc8fa71b97)) 74 | 75 | ## [1.5.0](https://github.com/upjs/facile-validator/compare/v1.4.0...v1.5.0) (2022-08-09) 76 | 77 | ### Features 78 | 79 | - add support for textarea validation ([76482eb](https://github.com/upjs/facile-validator/commit/76482eb8fe4dab0372b27a82f6765277eaec93fb)) 80 | 81 | ### Bug Fixes 82 | 83 | - use `unknown` instead of `any` ([41baeb5](https://github.com/upjs/facile-validator/commit/41baeb505a32c3b09b02c133080a5bdc8fa71b97)) 84 | 85 | ## [1.4.0](https://github.com/upjs/facile-validator/compare/v1.2.0...v1.4.0) (2022-07-31) 86 | 87 | ### Features 88 | 89 | - add support for validate per field ([3f508ff](https://github.com/upjs/facile-validator/commit/3f508ff2ce29475f23951051bf11aa284253facf)) 90 | - before release refactor ([7a9ef8a](https://github.com/upjs/facile-validator/commit/7a9ef8a7f0163053b108c5d6070f7229b04043f0)) 91 | - fix events trigger order ([c5536ab](https://github.com/upjs/facile-validator/commit/c5536abe999b1e6b33bf046a1d44e92745445b86)) 92 | - refactor project ([7bad2f5](https://github.com/upjs/facile-validator/commit/7bad2f5e9c4da35251f55d5b3eaa07ec75f26eb0)) 93 | - rename function ([a65c223](https://github.com/upjs/facile-validator/commit/a65c223a8e1758e16f72bd34eac2d545eb26a923)) 94 | - update renovate configs ([618a80b](https://github.com/upjs/facile-validator/commit/618a80bb4dcea9b41c9759c06dccea8b254c00dd)) 95 | - update version ([38ba534](https://github.com/upjs/facile-validator/commit/38ba5341a2346dda756fc9c4c758769273302edf)) 96 | - use name `container` for parent element ([570d6ff](https://github.com/upjs/facile-validator/commit/570d6ff475d85e78973b1bcdfb5171f7bce3d2cc)) 97 | 98 | ## [1.2.0](https://github.com/upjs/facile-validator/compare/v1.1.1...v1.2.0) (2022-05-26) 99 | 100 | ### Features 101 | 102 | - add lang for regex rule ([c6e2183](https://github.com/upjs/facile-validator/commit/c6e2183e17611ace4e7d9f5dbeae37369da8626d)) 103 | 104 | ### [1.1.1](https://github.com/upjs/facile-validator/compare/v1.1.0...v1.1.1) (2022-05-26) 105 | 106 | ## [1.1.0](https://github.com/upjs/facile-validator/compare/v1.0.0...v1.1.0) (2022-05-25) 107 | 108 | ### Features 109 | 110 | - add support for `regex` rule ([0390a52](https://github.com/upjs/facile-validator/commit/0390a52ca7bfa79dfc3993d148abec79abe0b71a)) 111 | - add UnoCSS to playground ([44769b9](https://github.com/upjs/facile-validator/commit/44769b997b12348fb73bab9a8bff301fc4c1afaf)) 112 | - add validator test ([c56495f](https://github.com/upjs/facile-validator/commit/c56495f9fb17964b4efb1f3f813c96d5244adc5b)) 113 | - enahnce x-rules feature ([8a40071](https://github.com/upjs/facile-validator/commit/8a400713121c9a9cb04cb41deb7fe4d373a82d53)) 114 | - improve `regex-rule` rule ([68f2990](https://github.com/upjs/facile-validator/commit/68f2990466174913ceabadb1ff3e63c17529293b)) 115 | 116 | ## [1.0.0](https://github.com/upjs/facile-validator/compare/v0.2.0...v1.0.0) (2022-04-07) 117 | 118 | ### Features 119 | 120 | - add `nullable` to readme.md ([c789615](https://github.com/upjs/facile-validator/commit/c789615c00038e2630145265ee7199534fa08676)) 121 | - add intro image ([79299e1](https://github.com/upjs/facile-validator/commit/79299e1ababd2cf798e181cb47dad5df328778cf)) 122 | - add nullable rule ([843926e](https://github.com/upjs/facile-validator/commit/843926e1f60e7f7ea87e079eed107c330223ba4a)) 123 | 124 | ### Bug Fixes 125 | 126 | - `nullable` now is a virtual rule ([1efa241](https://github.com/upjs/facile-validator/commit/1efa24137dd7b187c09d558deea8cad2333657ab)) 127 | - emit errors on build ([a29ff55](https://github.com/upjs/facile-validator/commit/a29ff5596aee192711a259beb5ba1ecaca375f8e)) 128 | - fix `nullable` behavior ([7a1d2a2](https://github.com/upjs/facile-validator/commit/7a1d2a2285ffe4772649f9b9220c3a0a5653b8fe)) 129 | - fix readme ([d7a84d3](https://github.com/upjs/facile-validator/commit/d7a84d3e6ad0a6da224209224387fa820443d89e)) 130 | - fix readme ([631e147](https://github.com/upjs/facile-validator/commit/631e1470e7ef788d493b6dcd8ec5a160c4041e54)) 131 | - fix typos in the readme ([c8ec46b](https://github.com/upjs/facile-validator/commit/c8ec46b6c6c3f44d1994106dd6ee1c6563b18e4e)) 132 | - ignore non-required and empty inputs ([c75ed73](https://github.com/upjs/facile-validator/commit/c75ed736eeb6231e89afd2018e1e78de52ed1ff3)) 133 | - remove `throwErrorWhen` helper function ([7d9b7a6](https://github.com/upjs/facile-validator/commit/7d9b7a6dca013dbc5e420219424f891bad07410c)) 134 | - remove unused functions ([d9ab937](https://github.com/upjs/facile-validator/commit/d9ab9373deb3e27f600ee1562efc1924d2c1f51b)) 135 | - replace intro image ([b6b6389](https://github.com/upjs/facile-validator/commit/b6b6389999791d63e77cb26a948ac1db70542d2f)) 136 | - undo code for non-required empty inputs ([a56eb6d](https://github.com/upjs/facile-validator/commit/a56eb6dfcd8991d045d589832d3698116cb064dc)) 137 | - undo code for non-required empty inputs ([60eb9c0](https://github.com/upjs/facile-validator/commit/60eb9c090540d9f1f4a11b128379b6ca568e16d0)) 138 | 139 | ## [0.2.0](https://github.com/upjs/facile-validator/compare/v0.1.2...v0.2.0) (2022-03-13) 140 | 141 | ### ⚠ BREAKING CHANGES 142 | 143 | - By this commit, an `HTMLElement` should be passed to the validator instead of a string 144 | 145 | ### Features 146 | 147 | - allow `HTMLElement` to be passed to the Validator ([c89b844](https://github.com/upjs/facile-validator/commit/c89b8443c94fc3158c8c19e9184f1234f09c5f7f)) 148 | - improve `within` to support array ([3168b46](https://github.com/upjs/facile-validator/commit/3168b462bdce3315267b5c5730ed847486175d30)) 149 | 150 | ### Bug Fixes 151 | 152 | - fix conflicts ([62ee440](https://github.com/upjs/facile-validator/commit/62ee44098255dcf1fd46b6d7779899643ca53246)) 153 | - fix conflicts ([d20892b](https://github.com/upjs/facile-validator/commit/d20892b9d3be906fac42573167694c3c362519a0)) 154 | - fix email rule ([61ec63a](https://github.com/upjs/facile-validator/commit/61ec63a4aa6fafe5b24e1bd1c2fc1e68fdde813d)) 155 | - fix optional events ([a34e2fe](https://github.com/upjs/facile-validator/commit/a34e2feb8f664e36026953721149716841c895f2)) 156 | - make `Events` fields optional ([062f9d1](https://github.com/upjs/facile-validator/commit/062f9d1ac3c3a87d5d02266a93e16329b606aa83)) 157 | - revert rules for national code ([dac6b55](https://github.com/upjs/facile-validator/commit/dac6b5564693a27ca5a957cc61e33be776ebc30e)) 158 | - use `HTMLElement` interface insteadof `HTMLInputElement` ([d6a23c2](https://github.com/upjs/facile-validator/commit/d6a23c27d5c81df41483f48c69e40d4b2e124929)) 159 | 160 | ### [0.1.2](https://github.com/upjs/facile-validator/compare/v0.1.1...v0.1.2) (2022-03-07) 161 | 162 | ### Bug Fixes 163 | 164 | - fix package entry ([a882e39](https://github.com/upjs/facile-validator/commit/a882e3943d922c8aef2a9531f990d441667674ce)) 165 | 166 | ### [0.1.1](https://github.com/upjs/facile-validator/compare/v0.1.0...v0.1.1) (2022-03-05) 167 | 168 | ## 0.1.0 (2022-03-05) 169 | 170 | ### ⚠ BREAKING CHANGES 171 | 172 | - use `data-rules` instead of `v-rules` 173 | 174 | ### Features 175 | 176 | - add `accepted` rule ([165ad31](https://github.com/upjs/facile-validator/commit/165ad311400c4c267c0394929a51e9499818ea9a)) 177 | - add `alpha-num-dash` rule ([e475ef5](https://github.com/upjs/facile-validator/commit/e475ef5fdcc3dd591b72eef09eaba598620dcfa7)) 178 | - add `alpha-num` and `num-dash` rules ([f3f1cb3](https://github.com/upjs/facile-validator/commit/f3f1cb3b58e867aff4eb33a4e6a27cda3b5be5eb)) 179 | - add `alpha` rule ([857b1a0](https://github.com/upjs/facile-validator/commit/857b1a00d4af8660a8430c33f55211650fcf637e)) 180 | - add `alpha` rule ([58fdb6b](https://github.com/upjs/facile-validator/commit/58fdb6b6746b429b86e8d6b3407e1cf3b9db60ac)) 181 | - add `createLang` function to support ts in custom langs ([f896802](https://github.com/upjs/facile-validator/commit/f89680274a4159d8552f5b2bb390af601128d421)) 182 | - add `createLang` function to support ts in custom langs ([11bacdb](https://github.com/upjs/facile-validator/commit/11bacdb78e688ff87950ce04c67353020e61125b)) 183 | - add `digits` rule ([4421af3](https://github.com/upjs/facile-validator/commit/4421af3a843b999c8cfa17dba0600dc0af88ebb6)) 184 | - add `endsWith` rule ([7fb1ecc](https://github.com/upjs/facile-validator/commit/7fb1ecc540f462d84da1d16449f0f5e9f0de1f2a)) 185 | - add `events` support ([5056dfc](https://github.com/upjs/facile-validator/commit/5056dfc0f90d599c792c9762dc35e78ee330ee66)) 186 | - add `gt` rule ([15b4ec6](https://github.com/upjs/facile-validator/commit/15b4ec6c49ae49d5e3a6cd8036e90f4b87c398ed)) 187 | - add `gte` rule ([dd96349](https://github.com/upjs/facile-validator/commit/dd9634923cdac34257d8d587aefdc492666b1135)) 188 | - add `integer` alias for int ([e630e8e](https://github.com/upjs/facile-validator/commit/e630e8e4baaf4caac43a6f59fdbf98971c0b94b6)) 189 | - add `Lang` type ([f7bb99e](https://github.com/upjs/facile-validator/commit/f7bb99e642c4b1cc04002c176a83c4eb97eba2be)) 190 | - add `length` rule ([8384bb2](https://github.com/upjs/facile-validator/commit/8384bb226b665b8a34fddf54eb9aa0e2021d3aee)) 191 | - add `lt` rule ([6ce701a](https://github.com/upjs/facile-validator/commit/6ce701acb30bf312defbc6159eac64e5811d5448)) 192 | - add `lte` rule ([2b21cf6](https://github.com/upjs/facile-validator/commit/2b21cf60f63591294c018d575f186bc36d4d1ff9)) 193 | - add `maxlen` & `minlen` & `len` alias ([8da8ac5](https://github.com/upjs/facile-validator/commit/8da8ac535ddb67a0a9c663c53274502608ad97fe)) 194 | - add `required-if` rule (without test) ([7497e71](https://github.com/upjs/facile-validator/commit/7497e71f75e6118de1a61b272c2c731f4b03d4e3)) 195 | - add `REQUIRED` RuleError to `between` rule ([284167d](https://github.com/upjs/facile-validator/commit/284167d0c32e86b0821febc9cc91e08d38e5762c)) 196 | - add `startsWith` rule ([1300995](https://github.com/upjs/facile-validator/commit/1300995365c8f7d2cc12f276718aac66086c09a4)) 197 | - add `validate:failed` event ([8d08aba](https://github.com/upjs/facile-validator/commit/8d08abaf21ee1d1ae1f55c9582f48055bf8aca27)) 198 | - add `within` rule ([b3eddaf](https://github.com/upjs/facile-validator/commit/b3eddaf984f314f04075768d7917ef189281ac49)) 199 | - add two `min` & `max` alias ([830eef6](https://github.com/upjs/facile-validator/commit/830eef619331d94be2043e816853241b69512c06)) 200 | - pass form as first argument of events ([cce6275](https://github.com/upjs/facile-validator/commit/cce6275b7ccb443f98c23cf9ac31d7659b23d4af)) 201 | - pass status to `validate:end` hook ([9f87f4c](https://github.com/upjs/facile-validator/commit/9f87f4ce916077386d07e35d55e680960f8d18dd)) 202 | - replace `throwErrorWhen` with `when` ([a18be2c](https://github.com/upjs/facile-validator/commit/a18be2c7323340f4d07c49b4c2c32eea58d095ad)) 203 | - support `async` validation ([b4ad388](https://github.com/upjs/facile-validator/commit/b4ad388a1aeb101418a40bb82b9f8d9235d06966)) 204 | - support `defaultOption` ([26611e7](https://github.com/upjs/facile-validator/commit/26611e74106adc8d718aca7013fd56e5a0c14340)) 205 | - support `events` with options ([2579900](https://github.com/upjs/facile-validator/commit/2579900676213a48c940a5e969d6b4489223aa25)) 206 | - support dependency in rules ([db8e620](https://github.com/upjs/facile-validator/commit/db8e62045cd9a16fa020709613f6a36e46198584)) 207 | - support for optional language ([f793c69](https://github.com/upjs/facile-validator/commit/f793c696b2ad39fc818fcc60196d9d89017fdc12)) 208 | - support for optional language ([f7d320e](https://github.com/upjs/facile-validator/commit/f7d320e3c533a6b909285074e1cea5b832109f5b)) 209 | - support multiple naming for rules ([bcac47a](https://github.com/upjs/facile-validator/commit/bcac47a540040a9f6393fe0b58b4f37976362187)) 210 | 211 | ### Bug Fixes 212 | 213 | - `throw error` must stop validating ([8e532f4](https://github.com/upjs/facile-validator/commit/8e532f4f015ecc85f567d84fe05b789f39f934eb)) 214 | - add more check on between argument ([6f501db](https://github.com/upjs/facile-validator/commit/6f501dbf3fef6ac0679318efb4122f96022dcbc9)) 215 | - add more tests for `alpha-num-dash` ([6369897](https://github.com/upjs/facile-validator/commit/6369897c61e8052499d5c760e0f61e1bcd9f4b89)) 216 | - edit error message ([92afda0](https://github.com/upjs/facile-validator/commit/92afda08e41ca72fcde913ed6f7b83eab6209e4b)) 217 | - edit incorrect texts ([c21f2f3](https://github.com/upjs/facile-validator/commit/c21f2f30a738afab6afc1e5d9a50f8aba868d47a)) 218 | - edit incorrect texts ([b14b2e7](https://github.com/upjs/facile-validator/commit/b14b2e7ee7d1874c37d68fd117d3fb7192b3dbd2)) 219 | - fix `max` rule bug ([76602ac](https://github.com/upjs/facile-validator/commit/76602ac523bff33bd8f3f2a51039414786fc5338)) 220 | - fix error orders ([caeb77a](https://github.com/upjs/facile-validator/commit/caeb77ac73634dc051afcc9cf05a0ba2d4667bca)) 221 | - fix incorrect error texts ([0389509](https://github.com/upjs/facile-validator/commit/03895090fd77d262851e66c1aca65a080ed2f4ee)) 222 | - fix lang function ([918a1e8](https://github.com/upjs/facile-validator/commit/918a1e8281094901a4322276493745eb7f6b55c7)) 223 | - fix playground `lang` imports bug ([952f7f7](https://github.com/upjs/facile-validator/commit/952f7f74923d8c3288dad476679758063bd83b13)) 224 | - fix type check for `langs` ([58864a6](https://github.com/upjs/facile-validator/commit/58864a684537d2a71ba3bde0c58956d23dab4983)) 225 | - minor core improvement ([c7455ea](https://github.com/upjs/facile-validator/commit/c7455ea160ef408f1e3368b641faf16ac3a4453c)) 226 | - remove `value` hardcode ([19ab1fa](https://github.com/upjs/facile-validator/commit/19ab1faeee0e3058c8a851c35b93ce7445bd147a)) 227 | - remove unused `REQUIRED_IF` error cause ([a9f8ec9](https://github.com/upjs/facile-validator/commit/a9f8ec942a1ff997c56c47fd9ff052663e625c58)) 228 | - rename `error-dev` constants ([64c1937](https://github.com/upjs/facile-validator/commit/64c193736c39840b5284c54f3674a25027e78f14)) 229 | - rename `Language.ts` ([94686a1](https://github.com/upjs/facile-validator/commit/94686a1cb3d6f136a4719a9db91dfcafd6ca2a90)) 230 | - rename Locale to Language ([fbb1668](https://github.com/upjs/facile-validator/commit/fbb16687500146f7298f6a0eb92fa1c1f836fab8)) 231 | - rename regexes ([90a876c](https://github.com/upjs/facile-validator/commit/90a876cb5280cf8bfd6cd02726762d4d2754b23a)) 232 | - rewrite `gte` to remove `between` dep ([03602dc](https://github.com/upjs/facile-validator/commit/03602dca89d626e3cac46d5ab5487779ffd3d719)) 233 | - rewrite `lte` to remove `between` dep ([436aae6](https://github.com/upjs/facile-validator/commit/436aae6f363f9c5a4e90d762856c5c94b9a4eee0)) 234 | - show right rule name ([dc3379c](https://github.com/upjs/facile-validator/commit/dc3379c781588af8fa8a9e1cc35a3fa6a36d7670)) 235 | - support args index ([8ae632f](https://github.com/upjs/facile-validator/commit/8ae632fd586ce87db9a91ff693fc5fc0a2e18824)) 236 | - support negative values for int ([b187270](https://github.com/upjs/facile-validator/commit/b187270c069466f244d89b22e159cec3140675f0)) 237 | - use `in` instead of `hasOwnProperty` ([8713e20](https://github.com/upjs/facile-validator/commit/8713e20c8849b06151b93cec3c4ef27a9571efda)) 238 | 239 | - rename `v-rules` to `data-rules` ([8917fc0](https://github.com/upjs/facile-validator/commit/8917fc009120e415645c3a5281b215386561636d)) 240 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ali Nazari 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 | # Facile Validator 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![verify](https://github.com/upjs/facile-validator/actions/workflows/verify.yml/badge.svg?branch=main)](https://github.com/upjs/facile-validator/actions/workflows/verify.yml) 6 | [![License][license-src]][license-href] 7 | 8 | Robust Frontend Form Validation, inspired by Laravel Validation, Built for Simplicity of Use 😋 9 | 10 |

11 | 12 |

13 | 14 | Facile (French word for "easy", pronounced `fa·sil`) is an HTML form validator that is inspired by Laravel's validation style and is designed for simplicity of use. 15 | 16 | **[DEMO](https://upjs.github.io/facile-validator/)** 17 | 18 | 19 | ## Table of Contents 20 | 21 | - [Installation](#installation) 22 | - [Usage](#usage) 23 | - [Validate on Field Change](#validate-on-field-change) 24 | - [Implementation in Frameworks](#implementation-in-frameworks) 25 | - [Handling Events](#handling-events) 26 | - [Available Validation Rules](#available-validation-rules) 27 | - [X-Prefixed Rules](#x-prefixed-rules) 28 | - [Localization](#localization) 29 | 30 | > Note: This package does not include any polyfills. If you want to use in old environments, add this package to your project's config transpiling list. 31 |
32 | 33 | ## Installation 34 | ```bash 35 | npm i @upjs/facile-validator 36 | 37 | # or 38 | yarn add @upjs/facile-validator 39 | ``` 40 | 41 |
42 | 43 | ## Usage 44 | HTML: 45 | 46 | ```html 47 |
48 | 49 |
50 | ``` 51 | 52 | The rules for each field are separated with a pipe character (vertical line) `|`. In this example, we've assigned 4 rules for that `input`: 53 | 54 | - bail 55 | - required 56 | - number 57 | - between (with two arguments: 1 and 10) 58 | 59 |
60 | 61 | JavaScript: 62 | 63 | ```javascript 64 | import { Validator, enLang as en } from '@upjs/facile-validator'; 65 | 66 | // Select the container element that contains the fields 67 | const form = document.querySelector('form'); 68 | 69 | // Create an instance of Validator for the container element 70 | const v = new Validator(form, { 71 | lang: en, 72 | }); 73 | 74 | form.addEventListener('submit', (e) => { 75 | e.preventDefault(); 76 | 77 | // Call validate method to start validation 78 | v.validate(); 79 | }); 80 | 81 | 82 | // Handle error-free validation 83 | v.on('validation:success', () => { 84 | alert('Nice! The form was validated without any errors'); 85 | }); 86 | 87 | // Handle failed validation 88 | v.on('validation:failed', () => { 89 | alert('Oops! There are some errors in the form.'); 90 | }); 91 | ``` 92 | 93 | Now every input with `data-rules` attribute in the `form` will be validated. 94 | 95 |
96 | 97 | ### Validate on Field Change 98 | _New in version 1.6_ 99 | By default, the validation starts when the `validate` method is called. If you want to validate a field as soon as it changes (e.g. when user starts typing) you can set the value of `onFieldChangeValidation` option to `true` in the validator's options: 100 | ```js 101 | const v = new Validator(form, { 102 | onFieldChangeValidation: true, 103 | } 104 | ``` 105 | By doing this, the validation starts for the field that being changed after 500ms delay. However, you can change this delay by setting `onFieldChangeValidationDelay` in the options: 106 | ```js 107 | const v = new Validator(form, { 108 | onFieldChangeValidation: true, 109 | onFieldChangeValidationDelay: 1000 // 1 Second 110 | } 111 | ``` 112 |
113 | 114 | ### Implementation in Frameworks 115 | - [React.js](/misc/code-examples/react.md) 116 | - _Others soon..._ 117 | 118 |
119 | 120 | ## Handling Events 121 | When the validation starts, ends, succeeds or fails, there are easy ways to handle these events. We do this with the help of the **Hooks**. 122 | A hook is simply a function that you define to be executed when a particular event occurs. 123 | 124 | There are five type of events that can be handled with the hooks: 125 | - [`validation:start`](#validationstart) 126 | - [`validation:end`](#validationend) 127 | - [`validation:success`](#validationsuccess) 128 | - [`validation:failed`](#validationfailed) 129 | - [`field:error`](#fielderror) 130 | 131 | 132 | To attach a hook to these events, use `on` method: 133 | ```javascript 134 | v.on(event_name, () => { 135 | // This function will be executed when the respective event occurs. 136 | }); 137 | ``` 138 | 139 | You can also attach those hooks in the config object: 140 | ```javascript 141 | const v = new Validator(form, { 142 | // ... 143 | on: { 144 | 'validation:success': () => { 145 | alert('Success! Form validated with no errors'); 146 | }, 147 | 'validation:failed': () => { 148 | alert('failed.'); 149 | }, 150 | }, 151 | }); 152 | ``` 153 | 154 | --- 155 | 156 | #### `validation:start` 157 | As you might have guessed, this event will occur when the validation starts: 158 | ```javascript 159 | v.on('validation:start', (form) => { 160 | // This function will be executed when the validation starts 161 | }); 162 | ``` 163 | --- 164 | 165 | #### `validation:end` 166 | This event will occur when the validation ends, no matter if it was successful or not: 167 | ```javascript 168 | v.on('validation:end', (form, isSuccessful) => { 169 | // This function will be executed when the validation ends 170 | }); 171 | ``` 172 | --- 173 | 174 | #### `validation:success` 175 | This event will occur when the validation ends with no errors: 176 | ```javascript 177 | v.on('validation:success', (form) => { 178 | // Do something after successful validation e.g. send the form-data to the server 179 | }); 180 | ``` 181 | --- 182 | 183 | #### `validation:failed` 184 | This event will occur when the validation ends while there are some errors in the form: 185 | ```javascript 186 | v.on('validation:failed', (form) => { 187 | // Notify the user to fix the form 188 | }); 189 | ``` 190 | --- 191 | 192 | #### `field:error` 193 | When a particular field has errors, you can handle the errors with this event: 194 | ```javascript 195 | v.on('field:error', (form, field, errors) => { 196 | errors.forEach(error => { 197 | console.log(error.args); 198 | console.log(error.message); 199 | console.log(error.rule); 200 | console.log(error.element); 201 | }); 202 | }); 203 | ``` 204 | This is a good place to display errors in your own format. By default, the validator automatically shows error messages below each input. However, you can disable this feature by setting `renderErrors` option to `false` in the config object: 205 | ```javascript 206 | const v = new Validator(form, { 207 | renderErrors: false, 208 | }); 209 | ``` 210 | 211 |
212 | 213 | ## Available Validation Rules: 214 | 215 | - [accepted](#accepted) 216 | - [alpha](#alpha) 217 | - [alpha-num](#alpha-num) 218 | - [alpha-num-dash](#alpha-num-dash) 219 | - [bail](#bail) 220 | - [between](#between) 221 | - [digits](#digits) 222 | - [email](#email) 223 | - [ends-with](#ends-with) 224 | - [int](#int) 225 | - [max](#max) 226 | - [min](#min) 227 | - [num-dash](#num-dash) 228 | - [number](#number) 229 | - [nullable](#nullable) 230 | - [regex](#regex) 231 | - [required](#required) 232 | - [size](#size) 233 | - [starts-with](#starts-with) 234 | - [in](#in) 235 | - ... 236 | - [Your rule?](https://github.com/upjs/facile-validator/pulls) 237 | 238 | --- 239 | 240 | ### accepted 241 | 242 | The field under validation (checkbox, radio) must be checked: 243 | 244 | ```html 245 | 246 | ``` 247 | 248 | --- 249 | 250 | ### alpha 251 | 252 | The field under validation must contain only alphabetic characters: 253 | 254 | ```html 255 | 256 | ``` 257 | Some valid inputs: 258 | - Hello 259 | - français 260 | - سلام 261 | --- 262 | 263 | ### alpha-num 264 | 265 | The field under validation must contain only alpha-numeric characters: 266 | 267 | ```html 268 | 269 | ``` 270 | Some valid inputs: 271 | - abc123 272 | - abc 273 | - 123 274 | --- 275 | 276 | ### alpha-num-dash 277 | 278 | The field under validation must contain only alpha-numeric characters, dashes, and underscores: 279 | 280 | ```html 281 | 282 | ``` 283 | 284 | Some valid inputs 285 | - abc-123 286 | - abc_123 287 | - abc123 288 | - abc 289 | - 123 290 | 291 | --- 292 | 293 | ### bail 294 | 295 | Stops running validation rules for the field after the first validation failure: 296 | 297 | ```html 298 | 299 | ``` 300 | 301 | _`required` rule will be processed and if it fails, other rules will not be processed._ 302 | 303 | --- 304 | 305 | ### between 306 | 307 | The field under validation must be a number between the given range: 308 | 309 | ```html 310 | 311 | ``` 312 | 313 | _The numbers lower than 1 and higher than 10 are not accepted._ 314 | 315 | --- 316 | 317 | ### digits 318 | 319 | The field under validation must be a number with the given length: 320 | 321 | ```html 322 | 323 | ``` 324 | 325 | _Only a number with the length of 10 is accepted (e.g. 1234567890)_ 326 | 327 | --- 328 | 329 | ### email 330 | 331 | The field under validation must be an email: 332 | 333 | ```html 334 | 335 | ``` 336 | 337 | --- 338 | 339 | ### ends-with 340 | 341 | The field under validation must end with the given substring: 342 | 343 | ```html 344 | 345 | ``` 346 | 347 | _Only the words that end with ies (technologies, parties, allies, ...) are accepted._ 348 | 349 | --- 350 | 351 | ### int 352 | 353 | The field under validation must be an integer (positive or negative): 354 | 355 | ```html 356 | 357 | ``` 358 | 359 | _You can also use `integer` rule._ 360 | 361 | --- 362 | 363 | ### max 364 | 365 | This rule is used for multiple purposes. 366 | 367 | In the combination with the `number` rule, the field under validation must be a number less than or equal to the given number: 368 | 369 | ```html 370 | 371 | ``` 372 | 373 | _Only a number less than or equal to 50 are accepted._ 374 | 375 | If `max` is used without `number` rule, the field under validation is considered as a `string` and then the field under validation must be a string with a maximum length of the given number: 376 | 377 | ```html 378 | 379 | ``` 380 | 381 | _Only strings with the length of 5 or less are accepted._ 382 | 383 | --- 384 | 385 | ### min 386 | 387 | This rule is used for multiple purposes. 388 | 389 | In the combination with the `number` rule, the field under validation must be a number greater than or equal to the given number: 390 | 391 | ```html 392 | 393 | ``` 394 | 395 | _Only a number greater than or equal to 50 will be accepted._ 396 | 397 | If `min` rule is used without `number` rule, the field under validation is considered as a string and then The field under validation must be a string with a minimum length of the given number. 398 | 399 | ```html 400 | 401 | ``` 402 | 403 | _Only strings with the length of 5 or higher will be accepted._ 404 | 405 | --- 406 | 407 | ### nullable 408 | The field under validation can be empty: 409 | 410 | ```html 411 | 412 | ``` 413 | `min` rule will not be processed unless the field is filled. Note that the rules order is important. In this example, if `nullable` rule comes after `min` rule, the validator first processes `min` rule and then `nullable` rule. 414 | 415 | --- 416 | 417 | ### num-dash 418 | 419 | The field under validation must contain only numeric characters, dashes, and underscores: 420 | 421 | ```html 422 | 423 | ``` 424 | 425 | _1000, 123-456, 123_456 are some valid inputs for this rule._ 426 | 427 | --- 428 | 429 | ### number 430 | 431 | The field under validation must be a number: 432 | 433 | ```html 434 | 435 | ``` 436 | 437 | --- 438 | 439 | ### regex 440 | _New in version 1.1_ 441 | The field under validation must match the given regular expression: 442 | ```html 443 | 444 | ``` 445 | To handle the regex rule in a more convenient way, see [#x-prefixed-rules](#x-prefixed-rules). 446 | 447 | --- 448 | 449 | ### required 450 | 451 | The field under validation must not be empty: 452 | 453 | ```html 454 | 455 | ``` 456 | 457 | --- 458 | 459 | ### size 460 | 461 | This rule is used for multiple purposes. 462 | 463 | In the combination with the `number` rule, the field under validation must be a number equal to the given number: 464 | 465 | ```html 466 | 467 | ``` 468 | 469 | _Only 1000 is accepted._ 470 | 471 | If used without `number` rule, the field under validation is considered as a string and then the field under validation must be a string with the exact length of the given argument: 472 | 473 | ```html 474 | 475 | ``` 476 | 477 | _Only the strings with the length of 5 are accepted._ 478 | 479 | --- 480 | 481 | ### starts-with 482 | 483 | The field under validation must start with the given substring: 484 | 485 | ```html 486 | 487 | ``` 488 | 489 | _Only the words that start with app (apple, application, append, ...) are accepted._ 490 | 491 | --- 492 | 493 | ### in 494 | 495 | The field under validation must be in the list of the given arguments: 496 | 497 | ```html 498 | 499 | ``` 500 | 501 | _Only red or green or blue are valid inputs._ 502 | 503 | `in` rule can also be used with a ` 507 | 508 | 509 | 510 | 511 | ``` 512 | 513 | _Only 1, 3 or both are accepted._ 514 | 515 | --- 516 | 517 |
518 | 519 | ## X-Prefixed Rules 520 | _New in version 1.1_ 521 | In some situations, passing the rule argument in HTML is not a good idea. For example for a complex argument for `regex` rule that can make the HTML code less legible: 522 | ```html 523 | 524 | ``` 525 | So instead of passing rules arguments in HTML, you can pass them in JavaScript code with the help of **X-Prefixed Rules**. All available rules can be prefixed with an `x-`. For example the `regex` rule can be written as `x-regex`. In this situation, the only argument for these rules points a key in `xRules` object in the configuration object: 526 | ```html 527 | 528 | ``` 529 | ```js 530 | const v = new Validator(form, { 531 | xRules: { 532 | zipcode: /^([0-9]{5})-([0-9]{5})$/, 533 | } 534 | }); 535 | ``` 536 | In this example, the final argument for `x-regex` rule is the value of `zipcode` property in `xRules` object. 537 | 538 | ### Custom error messages 539 | _New in version 1.12.0_ 540 | 541 | Previously, if an x-regex rule failed, a generic error message 'The value doesn't match the pattern' was displayed. For more convenience, you can now show your own error messages based on arbitrary conditions. To do so, use the `x-regex` rule like the below (see `password`): 542 | ```js 543 | const v = new Validator(form, { 544 | xRules: { 545 | // Simple x-regex rule with pre-defined error 546 | zipcode: /^([0-9]{5})-([0-9]{5})$/, 547 | 548 | // An x-regex rule with customized errors 549 | password: { 550 | value: /^(?=.*[A-Z])(?=.*[a-z])(?=.*[@#$%^&*]).{8,}$/, 551 | errorMessage: (field) => { 552 | if (field.value.length < 8) { 553 | return "Password must be at least 8 characters"; 554 | } 555 | 556 | if (!/[A-Z]/.test(field.value)) { 557 | return "Password must contain at least one uppercase letter"; 558 | } 559 | 560 | if (!/[a-z]/.test(field.value)) { 561 | return "Password must contain at least one lowercase letter"; 562 | } 563 | 564 | if (!/[@#$%^&*]/.test(field.value)) { 565 | return "Password must contain at least one special character"; 566 | } 567 | 568 | return "My custom error message"; 569 | }, 570 | }, 571 | } 572 | }); 573 | ``` 574 | Open [the demo](https://upjs.github.io/facile-validator/) and play with the password field. 575 | 576 | --- 577 | 578 |
579 | 580 | ## Localization 581 | When instantiating the `Validator` class, importing a language is mandatory. This allows you to keep the bundle size as minimal as possible by including only the desired language: 582 | ```javascript 583 | import { Validator, enLang as en } from '@upjs/facile-validator'; 584 | 585 | const form = document.querySelector('form'); 586 | const v = new Validator(form, { 587 | lang: en, 588 | }); 589 | ``` 590 | 591 | Facile Validator currently supports these languages by default: 592 | - English (import with `enLang`) 593 | - Persian (import with `faLang`) 594 | - Italian (import with `itLang`) (Since v1.8) 595 | - Chinese (import with `zhLang`) (Since v1.8) 596 | - French (import with `frLang`) (Since v1.8) 597 | - German (import with `deLang`) (Since v1.8) 598 | - Czech (import with `csLang`) (Since v1.9) 599 | - Dutch (import with `nlLang`) (Since v1.10) 600 | 601 | We welcome any contributions for other languages. The languages are located in [this path](https://github.com/upjs/facile-validator/blob/main/src/locales). Just copy any file, translate it into your own language and then make a **PR**. 602 | 603 |
604 | 605 | ### Using your own language 606 | Use `createLang` function to define your own error messages: 607 | ```javascript 608 | import { Validator, enLang as en, createLang } from '@upjs/facile-validator'; 609 | 610 | const myLang = createLang({ 611 | required: 'Please fill out this field', 612 | accepted: 'Please accept this field', 613 | }); 614 | 615 | const v = new Validator(form, { 616 | lang: myLang, 617 | }); 618 | ``` 619 | Note that in this case you should define a message for each existing rule. Although, to override only certain messages, pass the original language object: 620 | ```javascript 621 | import { Validator, enLang as en, createLang } from '@upjs/facile-validator'; 622 | 623 | const myLang = createLang({ 624 | ...en, 625 | required: 'Please fill out this field', 626 | accepted: 'Please accept this field', 627 | }); 628 | ``` 629 | 630 |
631 | 632 | ### Change the Language on the fly 633 | _New in version 1.8.0_ 634 | You can change the current language on runtime by using `setLanguage` method from the validator: 635 | ```javascript 636 | import { Validator, enLang as en, frLang as fr } from '@upjs/facile-validator'; 637 | 638 | const v = new Validator(form, { 639 | lang: en, 640 | }); 641 | 642 | // e.g. onclick 643 | v.setLanguage(fr); 644 | ``` 645 | 646 | 647 | ## License 648 | 649 | MIT 650 | 651 | 652 | [npm-version-src]: https://img.shields.io/npm/v/@upjs/facile-validator/latest.svg 653 | [npm-version-href]: https://npmjs.com/package/@upjs/facile-validator 654 | 655 | [npm-downloads-src]: https://img.shields.io/npm/dt/@upjs/facile-validator.svg 656 | [npm-downloads-href]: https://npmjs.com/package/@upjs/facile-validator 657 | 658 | [github-actions-ci-src]: https://github.com/upjs/facile-validator/workflows/verify/badge.svg 659 | [github-actions-ci-href]: https://github.com/upjs/facile-validator/actions/workflows/verify.yml 660 | 661 | [license-src]: https://img.shields.io/npm/l/@upjs/facile-validator.svg 662 | [license-href]: https://npmjs.com/package/@upjs/facile-validator 663 | 664 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineBuildConfig } from 'unbuild'; 3 | 4 | export default defineBuildConfig({ 5 | declaration: true, 6 | entries: ['src/index'], 7 | alias: { 8 | '@': resolve(__dirname, './src'), 9 | '~': resolve(__dirname, './playground'), 10 | }, 11 | rollup: { 12 | emitCJS: true, 13 | inlineDependencies: true, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /misc/code-examples/react.md: -------------------------------------------------------------------------------- 1 | You can easily integrate Facile Validator into an existing React project by utilizing the `useEffect` and `useRef` hooks 2 | 3 | 1. Create a ref object by using `useRef` hook 4 | 2. Attach the ref to the container element (e.g. `
` element) 5 | 3. Add implementation logic inside a `useEffect` hook with the ref object as a dependency 6 | 7 | ```tsx 8 | import { Validator, enLang as en } from '@upjs/facile-validator'; 9 | import { useEffect, useRef } from 'react'; 10 | 11 | export function MyReactComponent() { 12 | const form = useRef(null); 13 | 14 | useEffect(() => { 15 | if (!form.current) return; 16 | 17 | const v = new Validator(form.current, { 18 | lang: en, 19 | }); 20 | 21 | form.current.addEventListener('submit', (e) => { 22 | e.preventDefault(); 23 | 24 | // Call validate method to start validation 25 | v.validate(); 26 | }); 27 | 28 | v.on('validation:success', () => { 29 | alert('Nice! The form was validated without any errors'); 30 | }); 31 | }, [form]); 32 | 33 | return ( 34 | <> 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /misc/intro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upjs/facile-validator/6c9fb4019f2eb6d25d0547cad80cd4504e3d4bbd/misc/intro.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@upjs/facile-validator", 3 | "version": "1.12.3", 4 | "description": "Easy HTML form validator written in TypeScript with tree-shaking", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/upjs/facile-validator.git" 8 | }, 9 | "homepage": "https://github.com/upjs/facile-validator", 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "vite --config ./playground/vite.config.ts", 13 | "deploy": "vite --config ./playground/vite.config.ts build", 14 | "build": "tsc && unbuild", 15 | "lint": "eslint --ext .ts,vue --ignore-path .gitignore .", 16 | "test": "vitest --run", 17 | "test:watch": "vitest --watch", 18 | "coverage": "vitest run --coverage", 19 | "prepare": "husky install", 20 | "release": "yarn test && npx standard-version && git push --follow-tags && npm publish --access public" 21 | }, 22 | "lint-staged": { 23 | "*.ts": "eslint --fix", 24 | "*": "prettier -w -u" 25 | }, 26 | "keywords": [ 27 | "javascript", 28 | "typescript", 29 | "html", 30 | "validation", 31 | "validator", 32 | "form", 33 | "form-validation" 34 | ], 35 | "author": "Ali Nazari ", 36 | "contributors": [ 37 | "Mohammad Saleh Fadaei (https://twitter.com/ms_fadaei)" 38 | ], 39 | "type": "module", 40 | "sideEffects": false, 41 | "exports": { 42 | ".": { 43 | "types": "./dist/index.d.ts", 44 | "import": "./dist/index.mjs", 45 | "require": "./dist/index.cjs" 46 | } 47 | }, 48 | "main": "./dist/index.cjs", 49 | "module": "./dist/index.mjs", 50 | "types": "./dist/index.d.ts", 51 | "files": [ 52 | "src", 53 | "dist" 54 | ], 55 | "devDependencies": { 56 | "@babel/types": "7.27.0", 57 | "@types/node": "22.14.0", 58 | "@types/ws": "8.18.1", 59 | "@typescript-eslint/eslint-plugin": "6.21.0", 60 | "@typescript-eslint/parser": "6.21.0", 61 | "@unocss/preset-wind": "0.65.4", 62 | "@unocss/reset": "0.65.4", 63 | "c8": "8.0.1", 64 | "eslint": "8.57.1", 65 | "eslint-config-prettier": "9.1.0", 66 | "eslint-plugin-prettier": "4.2.1", 67 | "husky": "8.0.3", 68 | "lint-staged": "13.0.3", 69 | "path": "0.12.7", 70 | "prettier": "2.7.1", 71 | "standard-version": "9.5.0", 72 | "typescript": "5.8.3", 73 | "unbuild": "1.2.1", 74 | "unocss": "0.65.4", 75 | "vite": "5.4.17", 76 | "vitest": "0.34.6" 77 | }, 78 | "bugs": { 79 | "url": "https://github.com/upjs/facile-validator/issues" 80 | }, 81 | "directories": { 82 | "test": "tests" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Facile Validator - Demo 8 | 9 | 10 | 11 | 12 |
13 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 31 |
32 | 33 |
34 | 38 | 46 |
47 | 48 |
49 | 53 | 60 |
61 | 62 |
63 | 64 | 71 | 72 | • 8 Characters minimum 73 |
74 | • At least one uppercase letter 75 |
76 | • At least one lowercase letter 77 |
78 | • At least one special character (e.g. @, #, $, %, &, *, etc.) 79 |
80 |
81 | 82 |
83 | 86 | 87 |
88 | 89 |
90 | 91 | 97 |
98 | 99 |
100 | 101 | ⭐️ Star on Github 102 | 103 | 108 |
109 |
110 |

111 | Designed by 112 | Hex 113 | with ❤️ and ☕️ 114 |

115 |
116 | 117 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /playground/main.ts: -------------------------------------------------------------------------------- 1 | import '@unocss/reset/tailwind.css'; 2 | import 'uno.css'; 3 | import { Validator, enLang } from '@/index'; 4 | 5 | const form = document.querySelector('form') as HTMLElement; 6 | 7 | form.onsubmit = (e) => { 8 | e.preventDefault(); 9 | v.validate(); 10 | }; 11 | 12 | const v = new Validator(form, { 13 | lang: enLang, 14 | onFieldChangeValidation: true, 15 | onFieldChangeValidationDelay: 500, 16 | xRules: { 17 | zipcode: { 18 | value: '/^([0-9]{5})-([0-9]{5})$/', 19 | errorMessage: 'Invalid zipcode', 20 | }, 21 | password: { 22 | value: /^(?=.*[A-Z])(?=.*[a-z])(?=.*[@#$%^&*]).{8,}$/, 23 | errorMessage: (field) => { 24 | if (field.value.length < 8) { 25 | return 'Password must be at least 8 characters'; 26 | } 27 | 28 | if (!/[A-Z]/.test(field.value)) { 29 | return 'Password must contain at least one uppercase letter'; 30 | } 31 | 32 | if (!/[a-z]/.test(field.value)) { 33 | return 'Password must contain at least one lowercase letter'; 34 | } 35 | 36 | if (!/[@#$%^&*]/.test(field.value)) { 37 | return 'Password must contain at least one special character'; 38 | } 39 | 40 | return 'My custom error message'; 41 | }, 42 | }, 43 | }, 44 | on: { 45 | 'validation:success': () => { 46 | alert('Success! Form validated without any errors'); 47 | }, 48 | 'validation:end': () => { 49 | console.log('validation:end'); 50 | }, 51 | 'validation:start': () => { 52 | console.log('validation:start'); 53 | }, 54 | 'validation:failed': () => { 55 | console.log('validation:failed'); 56 | }, 57 | 'field:error': () => { 58 | console.log('field:error'); 59 | }, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import Unocss from 'unocss/vite'; 4 | import presetWind from '@unocss/preset-wind'; 5 | 6 | export default defineConfig({ 7 | root: resolve(__dirname, './'), 8 | resolve: { 9 | alias: { 10 | '@': resolve(__dirname, '../src'), 11 | '~': resolve(__dirname, './'), 12 | }, 13 | }, 14 | base: 'https://upjs.github.io/facile-validator/', 15 | plugins: [ 16 | Unocss({ 17 | presets: [presetWind()], 18 | theme: { 19 | colors: { 20 | primary: '#ef233c', 21 | heading: '#023047', 22 | }, 23 | fontFamily: { 24 | roboto: 'Roboto, sans-serif', 25 | }, 26 | }, 27 | shortcuts: { 28 | /* eslint-disable prettier/prettier */ 29 | input: 30 | 'mt-1 bg-gray-50 rounded-md px-3 py-1.5 transition transition-all outline-transparent focus:outline-primary border border-gray-100 focus:border-primary placeholder:text-sm text-heading', 31 | label: 'ml-2 text-heading text-base font-medium', 32 | 'label-not-inner': 'text-heading text-base font-medium', 33 | 'form-control': 'flex flex-col w-full', 34 | 'validator-err': 'text-primary text-sm mt-2 ml-2', 35 | /* eslint-enable prettier/prettier */ 36 | }, 37 | safelist: ['validator-err'], 38 | }), 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "schedule": "every weekend" 6 | } 7 | -------------------------------------------------------------------------------- /src/facile-validator.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/rules'; 2 | import { ValidatorOptions, EventsName, Events, FormInputElement, Lang } from '@/types'; 3 | import ValidatorError from '@/modules/validator-error'; 4 | import { getValue, toCamelCase, defaultErrorListeners, processRule } from '@/utils/helpers'; 5 | import EventBus from './modules/events'; 6 | import Language from './modules/language'; 7 | import { RuleError } from './modules/rule-error'; 8 | import { adaptRule } from './modules/rule-adapter'; 9 | 10 | type RuleKey = keyof typeof rules; 11 | 12 | const defaultOptions: ValidatorOptions = { 13 | renderErrors: true, 14 | onFieldChangeValidationDelay: 500, 15 | }; 16 | 17 | class Validator { 18 | private validatorError: ValidatorError; 19 | private events: EventBus; 20 | private options: ValidatorOptions; 21 | private container: HTMLElement; 22 | 23 | constructor(container: HTMLElement, options: ValidatorOptions = {}) { 24 | if (container === null || !(container instanceof HTMLElement)) { 25 | throw new Error('Invalid container element'); 26 | } 27 | 28 | this.options = Object.assign(defaultOptions, options); 29 | this.validatorError = new ValidatorError(); 30 | this.events = new EventBus(this.options.on); 31 | this.container = container; 32 | 33 | Language.set(this.options.lang); 34 | 35 | if (this.options.renderErrors) { 36 | defaultErrorListeners(this.events); 37 | } 38 | 39 | this.events.on('validation:start', () => this.validatorError.clearErrors()); 40 | this.events.on('validation:failed', () => this.triggerFieldErrorEvent()); 41 | 42 | if (options.onFieldChangeValidation) { 43 | this.validateOnFieldChange(); 44 | } 45 | } 46 | 47 | public validate(fields?: NodeListOf | FormInputElement[], shouldFireResultsEvent = true): boolean { 48 | this.events.call('validation:start', this.container); 49 | let isSuccessful = true; 50 | let status: 'success' | 'failed' = 'success'; 51 | 52 | if (fields === undefined) { 53 | fields = this.container.querySelectorAll('[data-rules]'); 54 | } 55 | 56 | if (fields.length > 0) { 57 | isSuccessful = this.validateFields(Array.from(fields)); 58 | status = isSuccessful ? 'success' : 'failed'; 59 | } 60 | 61 | this.events.call('validation:end', this.container, isSuccessful); 62 | 63 | if (shouldFireResultsEvent) { 64 | this.events.call(`validation:${status}`, this.container); 65 | } 66 | 67 | return isSuccessful; 68 | } 69 | 70 | public on(event: K, callback: Events[K]): void { 71 | this.events.on(event, callback); 72 | } 73 | 74 | public off(event: K, callback: Events[K]): void { 75 | this.events.off(event, callback); 76 | } 77 | 78 | private validateFields(fields: FormInputElement[]): boolean { 79 | for (const field of fields) { 80 | const fieldRules = field.getAttribute('data-rules')?.split('|'); 81 | 82 | if (fieldRules && fieldRules.length > 0) { 83 | const value = getValue(field); 84 | const shouldStopOnFirstFailure = this.shouldStopOnFirstFailure(fieldRules); 85 | const computedFieldRules = this.getComputedFieldRules(fieldRules, field); 86 | 87 | for (const fieldRule of computedFieldRules) { 88 | const { 89 | name: ruleName, 90 | argsValue: ruleArgs, 91 | customErrorMessage, 92 | } = processRule(fieldRule, this.options.xRules); 93 | const ruleKey = toCamelCase(ruleName) as RuleKey; 94 | 95 | if (this.isNullable(ruleKey) && value === '') { 96 | break; 97 | } 98 | 99 | if (ruleKey in rules) { 100 | try { 101 | const result = rules[ruleKey](value, ruleArgs); 102 | 103 | if (result instanceof RuleError) { 104 | let customMessage = ''; 105 | 106 | if (customErrorMessage) { 107 | customMessage = 108 | typeof customErrorMessage === 'function' ? customErrorMessage(field) : customErrorMessage; 109 | } 110 | this.validatorError.setError(field, ruleName, result, customMessage); 111 | if (shouldStopOnFirstFailure) { 112 | break; 113 | } 114 | } 115 | } catch (error) { 116 | console.error(new Error(`${ruleName}: ${(error as Error).message}`)); 117 | return false; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | return !this.validatorError.hasError; 125 | } 126 | 127 | private shouldStopOnFirstFailure(givenRules: Array) { 128 | return givenRules.includes('bail'); 129 | } 130 | 131 | private isNullable(givenRules: string) { 132 | return givenRules === 'nullable'; 133 | } 134 | 135 | private getComputedFieldRules(givenRules: string[], field: FormInputElement): string[] { 136 | return givenRules.map((rule) => adaptRule(rule, givenRules, field, this.container, this.options.xRules)); 137 | } 138 | 139 | private triggerFieldErrorEvent() { 140 | const totalErrors = this.validatorError.errors; 141 | 142 | totalErrors.forEach((fieldErrors) => { 143 | if (fieldErrors.length === 0) return; 144 | 145 | this.events.call('field:error', this.container, fieldErrors[0].element, fieldErrors); 146 | }); 147 | } 148 | 149 | private validateOnFieldChange() { 150 | let timeout: number; 151 | this.container.addEventListener('input', (event: Event) => { 152 | window.clearTimeout(timeout); 153 | const delay = this.options.onFieldChangeValidationDelay; 154 | 155 | timeout = window.setTimeout(() => { 156 | const target = event.target as FormInputElement; 157 | 158 | if (target.matches('[data-rules]')) { 159 | const result = this.validate([target], false); 160 | if (result === false) { 161 | this.triggerFieldErrorEvent(); 162 | } 163 | } 164 | }, delay); 165 | }); 166 | } 167 | 168 | public setLanguage(lang: Lang) { 169 | Language.set(lang); 170 | } 171 | } 172 | 173 | export default Validator; 174 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Validator } from '@/facile-validator'; 2 | export { 3 | en as enLang, 4 | fa as faLang, 5 | fr as frLang, 6 | de as deLang, 7 | it as itLang, 8 | zh as zhLang, 9 | cs as csLang, 10 | nl as nlLang, 11 | createLang, 12 | } from '@/locales'; 13 | -------------------------------------------------------------------------------- /src/locales/cs.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const csLang: Record = { 5 | [rules.ACCEPTED]: 'Přijměte toto pole', 6 | [rules.ALPHA]: 'Zadejte pouze abecední znaky', 7 | [rules.ALPHA_NUM]: 'Zadejte pouze alfanumerické znaky', 8 | [rules.ALPHA_NUM_DASH]: 'Zadejte pouze alfanumerické znaky, pomlčky a podtržítka', 9 | [rules.BETWEEN_LENGTH]: 'Hodnota musí mít $1 až $2 znaků', 10 | [rules.BETWEEN_NUMBER]: 'Zadejte číslo od $1 do $2', 11 | [rules.DIGITS]: 'Hodnota musí být číslo s $1 číslicemi', 12 | [rules.EMAIL]: 'Zadejte platnou emailovou adresu', 13 | [rules.ENDS_WITH]: 'Hodnota musí končit znaky „$1“', 14 | [rules.EQUAL_LENGTH]: 'Hodnota musí mít $1 znaků', 15 | [rules.EQUAL_NUMBER]: 'Hodnota musí být rovna $1', 16 | [rules.GREATER_EQUAL]: 'Zadejte číslo větší nebo rovné $1', 17 | [rules.INTEGER]: 'Hodnota musí být platné celé číslo', 18 | [rules.LESS_EQUAL]: 'Zadejte číslo menší nebo rovné $1', 19 | [rules.MAX_LENGTH]: 'Maximální délka je $1', 20 | [rules.MIN_LENGTH]: 'Minimální délka je $1', 21 | [rules.NUM_DASH]: 'Zadejte čísla s pomlčkami a podtržítky', 22 | [rules.NUMBER]: 'Zadejte platné číslo', 23 | [rules.REGEX]: 'Hodnota neodpovídá vzoru', 24 | [rules.REQUIRED]: 'Toto pole je povinné', 25 | [rules.STARTS_WITH]: 'Hodnota musí začínat znaky „$1“', 26 | [rules.WITHIN]: 'Hodnota je nesprávná', 27 | }; 28 | 29 | export default csLang; 30 | -------------------------------------------------------------------------------- /src/locales/de.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const deLang: Record = { 5 | [rules.ACCEPTED]: 'Bitte akzeptieren Sie dieses Feld', 6 | [rules.ALPHA]: 'Bitte geben Sie nur alphabetische Zeichen ein', 7 | [rules.ALPHA_NUM]: 'Bitte geben Sie nur alphanumerische Zeichen ein', 8 | [rules.ALPHA_NUM_DASH]: 'Bitte geben Sie nur alphanumerische Zeichen, Bindestriche und Unterstriche ein', 9 | [rules.BETWEEN_LENGTH]: 'Der Wert muss zwischen $1 und $2 Zeichen haben', 10 | [rules.BETWEEN_NUMBER]: 'Bitte geben Sie eine Zahl zwischen $1 und $2 ein', 11 | [rules.DIGITS]: 'Der Wert muss eine $1-stellige Zahl sein', 12 | [rules.EMAIL]: 'Bitte geben Sie eine gültige E-Mail-Adresse ein', 13 | [rules.ENDS_WITH]: 'Der Wert muss mit "$1" enden', 14 | [rules.EQUAL_LENGTH]: 'Der Wert muss $1 Zeichen haben', 15 | [rules.EQUAL_NUMBER]: 'Der Wert muss gleich $1 sein', 16 | [rules.GREATER_EQUAL]: 'Bitte geben Sie eine Zahl ein, die größer oder gleich $1 ist', 17 | [rules.INTEGER]: 'Der Wert muss eine gültige Ganzzahl sein', 18 | [rules.LESS_EQUAL]: 'Bitte geben Sie eine Zahl ein, die kleiner oder gleich $1 ist', 19 | [rules.MAX_LENGTH]: 'Maximale Länge ist $1', 20 | [rules.MIN_LENGTH]: 'Die Mindestlänge ist $1', 21 | [rules.NUM_DASH]: 'Bitte geben Sie Zahlen mit Bindestrichen und Unterstrichen ein', 22 | [rules.NUMBER]: 'Bitte geben Sie eine gültige Zahl ein', 23 | [rules.REGEX]: 'Der Wert stimmt nicht mit dem Muster überein', 24 | [rules.REQUIRED]: 'Dieses Feld ist erforderlich', 25 | [rules.STARTS_WITH]: 'Der Wert muss mit "$1" beginnen', 26 | [rules.WITHIN]: 'Der Wert ist falsch', 27 | }; 28 | 29 | export default deLang; 30 | -------------------------------------------------------------------------------- /src/locales/en.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const enLang: Record = { 5 | [rules.ACCEPTED]: 'Please accept this field', 6 | [rules.ALPHA]: 'Please enter only alphabetic characters', 7 | [rules.ALPHA_NUM]: 'Please enter only alpha-numeric characters', 8 | [rules.ALPHA_NUM_DASH]: 'Please enter only alpha-numeric characters, dashes, and underscores', 9 | [rules.BETWEEN_LENGTH]: 'The value must have between $1 and $2 characters', 10 | [rules.BETWEEN_NUMBER]: 'Please enter a number between $1 and $2', 11 | [rules.DIGITS]: 'The value must be a $1-digits number', 12 | [rules.EMAIL]: 'Please enter a valid email address', 13 | [rules.ENDS_WITH]: 'The value must ends with "$1"', 14 | [rules.EQUAL_LENGTH]: 'The value must have $1 characters', 15 | [rules.EQUAL_NUMBER]: 'The value must be equal to $1', 16 | [rules.GREATER_EQUAL]: 'Please enter a number greater than or equal to $1', 17 | [rules.INTEGER]: 'The value must be a valid integer', 18 | [rules.LESS_EQUAL]: 'Please enter a number less than or equal to $1', 19 | [rules.MAX_LENGTH]: 'Max length is $1', 20 | [rules.MIN_LENGTH]: 'Min length is $1', 21 | [rules.NUM_DASH]: 'Please enter numbers with dashes and underscores', 22 | [rules.NUMBER]: 'Please enter a valid number', 23 | [rules.REGEX]: "The value doesn't match the pattern", 24 | [rules.REQUIRED]: 'This field is required', 25 | [rules.STARTS_WITH]: 'The value must start with "$1"', 26 | [rules.WITHIN]: 'The value is incorrect', 27 | }; 28 | 29 | export default enLang; 30 | -------------------------------------------------------------------------------- /src/locales/fa.ts: -------------------------------------------------------------------------------- 1 | import { LangKeys } from '@/types'; 2 | import * as rules from '@/types/rules'; 3 | 4 | const faLang: Record = { 5 | [rules.ACCEPTED]: 'لطفا این فیلد را تیک بزنید', 6 | [rules.ALPHA]: 'لطفاً فقط حروف الفبا وارد کنید', 7 | [rules.ALPHA_NUM]: 'لطفاً فقط اعداد، زیر خط و خط فاصله وارد کنید', 8 | [rules.ALPHA_NUM_DASH]: 'لطفاً فقط حروف الفبا، اعداد، زیر خط و خط فاصله وارد کنید', 9 | [rules.BETWEEN_LENGTH]: 'مقدار باید بین $1 و $2 کاراکتر باشد', 10 | [rules.BETWEEN_NUMBER]: 'لطفا یک عدد بین $1 و $2 وارد کنید', 11 | [rules.DIGITS]: 'مقدار این فیلد باید $1 رقم باشد', 12 | [rules.EMAIL]: 'لطفا یک آدرس ایمیل معتبر وارد کنید', 13 | [rules.ENDS_WITH]: 'مقدار این فیلد باید با "$1" پایان داده شود', 14 | [rules.EQUAL_LENGTH]: 'مقدار این فیلد باید $1 حرف باشد', 15 | [rules.EQUAL_NUMBER]: 'مقدار این فیلد باید $1 باشد', 16 | [rules.GREATER_EQUAL]: 'لطفا یک عدد بزرگتر یا مساوی $1 وارد کنید', 17 | [rules.INTEGER]: 'مقدار این فیلد باید یک عدد صحیح باشد', 18 | [rules.LESS_EQUAL]: 'لطفا یک عدد کوچکتر یا مساوی $1 وارد کنید', 19 | [rules.MAX_LENGTH]: 'حداکثر طول مجاز این فیلد $1 است', 20 | [rules.MIN_LENGTH]: 'حداقل طول مجاز این فیلد $1 است', 21 | [rules.REGEX]: 'مقدار وارد شده با الگوی مشخص شده همخوانی ندارد', 22 | [rules.REQUIRED]: 'این فیلد الزامی است', 23 | [rules.NUM_DASH]: 'لطفاً فقط اعداد با زیرخط و خط فاصله وارد کنید', 24 | [rules.NUMBER]: 'لطفا یک عدد معتبر وارد کنید', 25 | [rules.STARTS_WITH]: 'مقدار این فیلد باید با "$1" شروع شود', 26 | [rules.WITHIN]: 'مقدار این فیلد نادرست است', 27 | }; 28 | 29 | export default faLang; 30 | -------------------------------------------------------------------------------- /src/locales/fr.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const frLang: Record = { 5 | [rules.ACCEPTED]: 'Veuillez accepter ce champ', 6 | [rules.ALPHA]: 'Veuillez saisir uniquement des caractères alphabétiques', 7 | [rules.ALPHA_NUM]: 'Veuillez saisir uniquement des caractères alphanumériques', 8 | [rules.ALPHA_NUM_DASH]: 9 | 'Veuillez saisir uniquement des caractères alphanumériques, des tirets et des caractères de soulignement', 10 | [rules.BETWEEN_LENGTH]: 'La valeur doit comporter entre $1 et $2 caractères', 11 | [rules.BETWEEN_NUMBER]: 'Veuillez saisir un nombre entre $1 et $2 caractères', 12 | [rules.DIGITS]: 'La valeur doit être un nombre à $1 chiffre', 13 | [rules.EMAIL]: 'Veuillez saisir une adresse électronique valide', 14 | [rules.ENDS_WITH]: 'La valeur doit se terminer par "$1"', 15 | [rules.EQUAL_LENGTH]: 'La valeur doit avoir des caractères de $1', 16 | [rules.EQUAL_NUMBER]: 'La valeur doit être égale à $1', 17 | [rules.GREATER_EQUAL]: 'Veuillez saisir un nombre supérieur ou égal à $1', 18 | [rules.INTEGER]: 'La valeur doit être un nombre entier valide', 19 | [rules.LESS_EQUAL]: 'Veuillez entrer un nombre inférieur ou égal à $1', 20 | [rules.MAX_LENGTH]: 'La longueur maximale est de $1', 21 | [rules.MIN_LENGTH]: 'La longueur minimale est de $1', 22 | [rules.NUM_DASH]: 'Veuillez saisir les chiffres avec des tirets et des caractères de soulignement', 23 | [rules.NUMBER]: 'Veuillez entrer un nombre valide', 24 | [rules.REGEX]: 'La valeur ne correspond pas au modèle', 25 | [rules.REQUIRED]: 'Ce champ est obligatoire', 26 | [rules.STARTS_WITH]: 'La valeur doit commencer par "$1"', 27 | [rules.WITHIN]: 'La valeur est incorrecte', 28 | }; 29 | 30 | export default frLang; 31 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { Lang } from '@/types'; 2 | 3 | export { default as en } from './en'; 4 | export { default as fa } from './fa'; 5 | export { default as fr } from './fr'; 6 | export { default as de } from './de'; 7 | export { default as it } from './it'; 8 | export { default as zh } from './zh'; 9 | export { default as cs } from './cs'; 10 | export { default as nl } from './nl'; 11 | 12 | export const createLang = (lang: Lang): Lang => lang; 13 | -------------------------------------------------------------------------------- /src/locales/it.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const itLang: Record = { 5 | [rules.ACCEPTED]: 'Si prega di accettare questo campo', 6 | [rules.ALPHA]: 'Inserire solo caratteri alfabetici', 7 | [rules.ALPHA_NUM]: 'Inserire solo caratteri alfanumerici', 8 | [rules.ALPHA_NUM_DASH]: 'Inserire solo caratteri alfanumerici, trattini e trattini bassi', 9 | [rules.BETWEEN_LENGTH]: 'Il valore deve essere compreso tra $1 e $2 caratteri', 10 | [rules.BETWEEN_NUMBER]: 'Inserire un numero compreso tra $1 e $2', 11 | [rules.DIGITS]: 'Il valore deve essere un numero di $1 cifra', 12 | [rules.EMAIL]: 'Inserire un indirizzo e-mail valido', 13 | [rules.ENDS_WITH]: 'Il valore deve terminare con "$1"', 14 | [rules.EQUAL_LENGTH]: 'Il valore deve avere caratteri da $1', 15 | [rules.EQUAL_NUMBER]: 'Il valore deve essere uguale a $1', 16 | [rules.GREATER_EQUAL]: 'Inserisci un numero maggiore o uguale a $1', 17 | [rules.INTEGER]: 'Il valore deve essere un numero intero valido', 18 | [rules.LESS_EQUAL]: 'Inserire un numero minore o uguale a $1', 19 | [rules.MAX_LENGTH]: 'La lunghezza massima è $1', 20 | [rules.MIN_LENGTH]: 'La lunghezza minima è $1', 21 | [rules.NUM_DASH]: 'Inserisci numeri con trattini e trattini bassi', 22 | [rules.NUMBER]: 'Inserire un numero valido', 23 | [rules.REGEX]: 'Il valore non corrisponde al modello', 24 | [rules.REQUIRED]: 'Questo campo è obbligatorio', 25 | [rules.STARTS_WITH]: 'Il valore deve iniziare con "$1"', 26 | [rules.WITHIN]: 'Il valore non è corretto', 27 | }; 28 | 29 | export default itLang; 30 | -------------------------------------------------------------------------------- /src/locales/nl.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const nlLang: Record = { 5 | [rules.ACCEPTED]: 'Accepteer dit veld a.u.b.', 6 | [rules.ALPHA]: 'Voer alleen alfabetische tekens in', 7 | [rules.ALPHA_NUM]: 'Alleen alfanumerieke tekens a.u.b', 8 | [rules.ALPHA_NUM_DASH]: 'Voer alleen alfanumerieke tekens, streepjes en underscores in', 9 | [rules.BETWEEN_LENGTH]: 'De waarde moet tussen $1 en $2 tekens liggen', 10 | [rules.BETWEEN_NUMBER]: 'Voer een getal tussen $1 en $2 in', 11 | [rules.DIGITS]: 'De waarde moet een getal van 1 cijfer zijn.', 12 | [rules.EMAIL]: 'Voer een geldig e-mailadres in', 13 | [rules.ENDS_WITH]: 'De waarde moet eindigen op "$1"', 14 | [rules.EQUAL_LENGTH]: 'De waarde moet uit $1 tekens bestaan', 15 | [rules.EQUAL_NUMBER]: 'De waarde moet gelijk zijn aan $1', 16 | [rules.GREATER_EQUAL]: 'Voer a.u.b. een getal in groter dan of gelijk aan $1', 17 | [rules.INTEGER]: 'De waarde moet een geldig geheel getal zijn', 18 | [rules.LESS_EQUAL]: 'Voer a.u.b. een getal in kleiner dan of gelijk aan $1', 19 | [rules.MAX_LENGTH]: 'Max. lengte is $1', 20 | [rules.MIN_LENGTH]: 'Min. lengte is $1', 21 | [rules.NUM_DASH]: 'Voer getallen met streepjes en underscores in.', 22 | [rules.NUMBER]: 'Voer een geldig getal in', 23 | [rules.REGEX]: 'De waarde komt niet overeen met het patroon', 24 | [rules.REQUIRED]: 'Dit veld is verplicht', 25 | [rules.STARTS_WITH]: 'De waarde moet beginnen met "$1"', 26 | [rules.WITHIN]: 'De waarde is onjuist', 27 | }; 28 | 29 | export default nlLang; 30 | -------------------------------------------------------------------------------- /src/locales/zh.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '@/types/rules'; 2 | import { LangKeys } from '@/types'; 3 | 4 | const zhLang: Record = { 5 | [rules.ACCEPTED]: '请接受此字段', 6 | [rules.ALPHA]: '请仅输入字母字符', 7 | [rules.ALPHA_NUM]: '请仅输入字母数字字符', 8 | [rules.ALPHA_NUM_DASH]: '请仅输入字母数字字符、破折号和下划线', 9 | [rules.BETWEEN_LENGTH]: '值必须介于 $1 和 $2 之间', 10 | [rules.BETWEEN_NUMBER]: '请输入一个介于 $1 和 $2 之间的数字', 11 | [rules.DIGITS]: '该值必须是 $1 位数', 12 | [rules.EMAIL]: '请输入有效的电子邮件地址', 13 | [rules.ENDS_WITH]: '值必须以“$1”结尾', 14 | [rules.EQUAL_LENGTH]: '值必须有 $1 个字符', 15 | [rules.EQUAL_NUMBER]: '值必须等于 $1', 16 | [rules.GREATER_EQUAL]: '请输入一个大于或等于 $1 的数字', 17 | [rules.INTEGER]: '该值必须是一个有效的整数', 18 | [rules.LESS_EQUAL]: '请输入一个小于或等于 $1 的数字', 19 | [rules.MAX_LENGTH]: '最大长度为 $1', 20 | [rules.MIN_LENGTH]: '最小长度为 $1', 21 | [rules.NUM_DASH]: '请输入带破折号和下划线的数字', 22 | [rules.NUMBER]: '请输入一个有效的数字', 23 | [rules.REGEX]: '该值与模式不匹配', 24 | [rules.REQUIRED]: '此字段是必需的', 25 | [rules.STARTS_WITH]: '值必须以“$1”开头', 26 | [rules.WITHIN]: '值不正确', 27 | }; 28 | 29 | export default zhLang; 30 | -------------------------------------------------------------------------------- /src/modules/events.ts: -------------------------------------------------------------------------------- 1 | import { Events, EventsList, EventsName, EventsOption } from '@/types'; 2 | 3 | export default class EventBus { 4 | private events: EventsList; 5 | 6 | constructor(events: EventsOption = {}) { 7 | this.events = {}; 8 | 9 | const keys = Object.keys(events) as EventsName[]; 10 | keys.forEach((key: K) => { 11 | if (typeof events[key] === 'function') { 12 | this.events[key] = []; 13 | (this.events[key] as Events[K][]).push(events[key] as Events[K]); 14 | } 15 | }); 16 | } 17 | 18 | public on(event: K, callback: Events[K]): void { 19 | if (!this.events[event]) { 20 | this.events[event] = []; 21 | } 22 | 23 | const events = this.events[event] as Events[K][]; 24 | events.push(callback); 25 | } 26 | 27 | public off(event: K, callback: Events[K]): void { 28 | if (typeof this.events[event] === 'undefined') { 29 | return; 30 | } 31 | 32 | const events = this.events[event] as Events[K][]; 33 | const index = events.indexOf(callback); 34 | 35 | if (index !== -1) { 36 | events.splice(index, 1); 37 | } 38 | } 39 | 40 | public call(event: K, ...args: Parameters): void { 41 | if (typeof this.events[event] !== 'undefined') { 42 | const events = this.events[event] as ((...args: Parameters) => void)[]; 43 | 44 | events.forEach((callback) => { 45 | callback(...args); 46 | }); 47 | } 48 | } 49 | 50 | public clear(): void { 51 | this.events = {}; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/language.ts: -------------------------------------------------------------------------------- 1 | import { Lang } from '@/types'; 2 | 3 | class Language { 4 | private lang?: Lang; 5 | 6 | public set(lang?: Lang) { 7 | this.lang = lang; 8 | } 9 | 10 | public get() { 11 | return typeof this.lang === 'object' ? this.lang : {}; 12 | } 13 | } 14 | 15 | export default new Language(); 16 | -------------------------------------------------------------------------------- /src/modules/rule-adapter.ts: -------------------------------------------------------------------------------- 1 | import { AdapterFn, FormInputElement, XRules } from '@/types'; 2 | import { getValue, processRule, toCamelCase } from '@/utils/helpers'; 3 | 4 | const mapMethods: Record = { 5 | requiredIf: prependTargetValue, 6 | between: prependType, 7 | size: prependType, 8 | min: prependType, 9 | max: prependType, 10 | in: prependType, 11 | }; 12 | 13 | export function adaptRule( 14 | rule: string, 15 | rules: string[], 16 | field: FormInputElement, 17 | container: HTMLElement, 18 | xRules?: XRules 19 | ): string { 20 | const ruleName = toCamelCase(processRule(rule, xRules).name); 21 | 22 | return mapMethods[ruleName]?.(rule, rules, field, container, xRules) || rule; 23 | } 24 | 25 | export function prependType( 26 | rule: string, 27 | rules: string[], 28 | _field: FormInputElement, 29 | _parentEl: HTMLElement, 30 | xRules?: XRules 31 | ): string { 32 | const { name, argsValue } = processRule(rule, xRules); 33 | 34 | const indexOfRule = rules.indexOf(rule); 35 | const rulesBeforeRule = rules.slice(0, indexOfRule); 36 | 37 | let type = 'string'; 38 | if (rulesBeforeRule.includes('number') || rulesBeforeRule.includes('int') || rulesBeforeRule.includes('integer')) { 39 | type = 'number'; 40 | } else if (rulesBeforeRule.includes('array')) { 41 | type = 'array'; 42 | } 43 | 44 | return `${name}:${type},${argsValue}`; 45 | } 46 | 47 | function prependTargetValue( 48 | rule: string, 49 | _rules: string[], 50 | _field: FormInputElement, 51 | _parentEl: HTMLElement, 52 | xRules?: XRules 53 | ): string { 54 | const { name, args } = processRule(rule, xRules); 55 | 56 | if (args.length === 0) return name; 57 | 58 | let targetValue = ''; 59 | if (args.length > 0) { 60 | const targetField = document.getElementById(args[0]); 61 | if (targetField !== null) { 62 | targetValue = getValue(targetField as FormInputElement); 63 | } 64 | } 65 | 66 | args.splice(0, 1, targetValue); 67 | 68 | return `${name}:${args.join(',')}`; 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/rule-error.ts: -------------------------------------------------------------------------------- 1 | export class RuleError extends Error { 2 | public args: string[]; 3 | 4 | constructor(cause: string, ...args: string[]) { 5 | super(cause); 6 | this.args = args; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/validator-error.ts: -------------------------------------------------------------------------------- 1 | import { ErrorDetail, FormInputElement } from '@/types'; 2 | import { lang } from '@/utils/helpers'; 3 | import Language from './language'; 4 | import { RuleError } from './rule-error'; 5 | 6 | export default class ValidatorError { 7 | public lang: Record; 8 | public errorsList: ErrorDetail[][]; 9 | 10 | constructor() { 11 | this.lang = Language.get(); 12 | this.errorsList = []; 13 | } 14 | 15 | public setError(element: FormInputElement, rule: string, ruleError: RuleError, customMessage = '') { 16 | let errors = this.errorsList.find((error) => error[0].element === element); 17 | 18 | if (!errors) { 19 | errors = []; 20 | this.errorsList.push(errors); 21 | } 22 | 23 | const errorMessage = customMessage || lang(ruleError.message, ...ruleError.args); 24 | 25 | const errorDetail: ErrorDetail = { 26 | message: errorMessage, 27 | element, 28 | rule, 29 | cause: ruleError.message, 30 | args: ruleError.args, 31 | }; 32 | 33 | errors.push(errorDetail); 34 | } 35 | 36 | public get hasError(): boolean { 37 | return Object.keys(this.errorsList).length > 0; 38 | } 39 | 40 | public get errors(): ErrorDetail[][] { 41 | return this.errorsList; 42 | } 43 | 44 | public clearErrors() { 45 | this.errorsList = []; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/rules/accepted.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { ACCEPTED } from '@/types/rules'; 4 | 5 | function accepted(value: string): true | RuleError { 6 | if (value === 'checked') { 7 | return true; 8 | } 9 | 10 | return new RuleError(ACCEPTED); 11 | } 12 | 13 | export default accepted as Rule; 14 | -------------------------------------------------------------------------------- /src/rules/alpha-num-dash.ts: -------------------------------------------------------------------------------- 1 | import { RuleError } from '@/modules/rule-error'; 2 | import { Rule } from '@/types'; 3 | import { alphaNumDash as alphaNumDashRegex } from '@/utils/regex'; 4 | import { ALPHA_NUM_DASH } from '@/types/rules'; 5 | 6 | function alphaNumDash(value: string): true | RuleError { 7 | return alphaNumDashRegex.test(value) || new RuleError(ALPHA_NUM_DASH); 8 | } 9 | 10 | export default alphaNumDash as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/alpha-num.ts: -------------------------------------------------------------------------------- 1 | import { RuleError } from '@/modules/rule-error'; 2 | import { Rule } from '@/types'; 3 | import { alphaNum as alphaNumRegex } from '@/utils/regex'; 4 | import { ALPHA_NUM } from '@/types/rules'; 5 | 6 | function alphaNum(value: string): true | RuleError { 7 | return alphaNumRegex.test(value) || new RuleError(ALPHA_NUM); 8 | } 9 | 10 | export default alphaNum as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/alpha.ts: -------------------------------------------------------------------------------- 1 | import { RuleError } from '@/modules/rule-error'; 2 | import { Rule } from '@/types'; 3 | import { alpha as alphaRegex } from '@/utils/regex'; 4 | import { ALPHA } from '@/types/rules'; 5 | 6 | function alpha(value: string): true | RuleError { 7 | return alphaRegex.test(value) || new RuleError(ALPHA); 8 | } 9 | 10 | export default alpha as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/between.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when, processArgs } from '@/utils/helpers'; 4 | import { BETWEEN_NUMBER, BETWEEN_LENGTH } from '@/types/rules'; 5 | import { ARGUMENT_MUST_BE_A_NUMBER, ARGUMENT_MUST_BE_POSITIVE, ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 6 | 7 | function between(value: string, args = ''): true | RuleError { 8 | const [type, minArg, maxArg] = processArgs(args); 9 | when(!type).throwError(ARGUMENT_MUST_BE_PROVIDED); 10 | when(!minArg || !maxArg).throwError(ARGUMENT_MUST_BE_PROVIDED); 11 | 12 | const min = Number(minArg); 13 | const max = Number(maxArg); 14 | when(Number.isNaN(min) || Number.isNaN(max)).throwError(ARGUMENT_MUST_BE_A_NUMBER); 15 | when(min > max).throwError('min must be less than max'); 16 | when(min === max).throwError('min and max must not be equal'); 17 | 18 | if (type === 'number') { 19 | return betweenForNumber(value, min, max); 20 | } else { 21 | return betweenForString(value, min, max); 22 | } 23 | } 24 | 25 | function betweenForNumber(value: string, min: number, max: number) { 26 | const valueInNumber = Number(value); 27 | if (value !== '' && !Number.isNaN(valueInNumber) && valueInNumber >= min && valueInNumber <= max) { 28 | return true; 29 | } 30 | 31 | return new RuleError(BETWEEN_NUMBER, String(min), String(max)); 32 | } 33 | 34 | function betweenForString(value: string, min: number, max: number) { 35 | when(min < 0 || max < 0).throwError(ARGUMENT_MUST_BE_POSITIVE); 36 | 37 | if (value.length >= min && value.length <= max) { 38 | return true; 39 | } 40 | 41 | return new RuleError(BETWEEN_LENGTH, String(min), String(max)); 42 | } 43 | 44 | export default between as Rule; 45 | -------------------------------------------------------------------------------- /src/rules/digits.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { int as isInteger } from '@/rules'; 4 | import { when } from '@/utils/helpers'; 5 | import { DIGITS } from '@/types/rules'; 6 | import { ARGUMENT_MUST_BE_AN_INTEGER, ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 7 | 8 | function digits(value: string, digitLength = ''): true | RuleError { 9 | when(digitLength === '').throwError(ARGUMENT_MUST_BE_PROVIDED); 10 | when(isInteger(digitLength) !== true || +digitLength < 1).throwError(ARGUMENT_MUST_BE_AN_INTEGER); 11 | 12 | const regex = new RegExp(`^-?[0-9]{${digitLength}}$`); 13 | 14 | return regex.test(value) ? true : new RuleError(DIGITS, digitLength); 15 | } 16 | 17 | export default digits as Rule; 18 | -------------------------------------------------------------------------------- /src/rules/email.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { email as emailRegex } from '@/utils/regex'; 4 | import { EMAIL } from '@/types/rules'; 5 | 6 | function email(value: string): true | RuleError { 7 | return emailRegex.test(value) || new RuleError(EMAIL); 8 | } 9 | 10 | export default email as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/ends-with.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when } from '@/utils/helpers'; 4 | import { ENDS_WITH } from '@/types/rules'; 5 | import { ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 6 | 7 | function endsWith(value: string, end = ''): true | RuleError { 8 | when(end === '').throwError(ARGUMENT_MUST_BE_PROVIDED); 9 | 10 | return value.endsWith(end) || new RuleError(ENDS_WITH, end); 11 | } 12 | 13 | export default endsWith as Rule; 14 | -------------------------------------------------------------------------------- /src/rules/index.ts: -------------------------------------------------------------------------------- 1 | export { default as accepted } from './accepted'; 2 | export { default as alpha } from './alpha'; 3 | export { default as alphaNum } from './alpha-num'; 4 | export { default as alphaNumDash } from './alpha-num-dash'; 5 | export { default as between } from './between'; 6 | export { default as digits } from './digits'; 7 | export { default as endsWith } from './ends-with'; 8 | export { default as email } from './email'; 9 | export { default as min } from './min'; 10 | export { default as integer, default as int } from './int'; 11 | export { default as max } from './max'; 12 | export { default as number } from './number'; 13 | export { default as numDash } from './num-dash'; 14 | export { default as regex } from './regex'; 15 | export { default as required } from './required'; 16 | export { default as requiredIf } from './required-if'; 17 | export { default as size } from './size'; 18 | export { default as startsWith } from './starts-with'; 19 | export { default as within, default as in } from './within'; 20 | -------------------------------------------------------------------------------- /src/rules/int.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { integer as integerRegex } from '@/utils/regex'; 4 | import { INTEGER } from '@/types/rules'; 5 | 6 | function int(value: string): true | RuleError { 7 | return integerRegex.test(value) || new RuleError(INTEGER); 8 | } 9 | 10 | export default int as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/max.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when, processArgs } from '@/utils/helpers'; 4 | import { ARGUMENT_MUST_BE_A_NUMBER, ARGUMENT_MUST_BE_POSITIVE, ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 5 | import { LESS_EQUAL, MAX_LENGTH } from '@/types/rules'; 6 | 7 | function max(value: string, args = ''): true | RuleError { 8 | const [type, max] = processArgs(args); 9 | when(!type).throwError(ARGUMENT_MUST_BE_PROVIDED); 10 | when(!max).throwError(ARGUMENT_MUST_BE_PROVIDED); 11 | 12 | const maxInNumber = Number(max); 13 | when(Number.isNaN(maxInNumber)).throwError(ARGUMENT_MUST_BE_A_NUMBER); 14 | 15 | if (type === 'number') { 16 | return maxForNumber(value, maxInNumber); 17 | } else { 18 | return maxForString(value, maxInNumber); 19 | } 20 | } 21 | 22 | function maxForNumber(value: string, max: number) { 23 | const valueInNumber = Number(value); 24 | if (value !== '' && !Number.isNaN(valueInNumber) && valueInNumber <= max) { 25 | return true; 26 | } 27 | 28 | return new RuleError(LESS_EQUAL, String(max)); 29 | } 30 | 31 | function maxForString(value: string, max: number) { 32 | when(max < 0).throwError(ARGUMENT_MUST_BE_POSITIVE); 33 | 34 | if (value.length <= max) { 35 | return true; 36 | } 37 | 38 | return new RuleError(MAX_LENGTH, String(max)); 39 | } 40 | 41 | export default max as Rule; 42 | -------------------------------------------------------------------------------- /src/rules/min.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when, processArgs } from '@/utils/helpers'; 4 | import { ARGUMENT_MUST_BE_A_NUMBER, ARGUMENT_MUST_BE_POSITIVE, ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 5 | import { GREATER_EQUAL, MIN_LENGTH } from '@/types/rules'; 6 | 7 | function min(value: string, args = ''): true | RuleError { 8 | const [type, min] = processArgs(args); 9 | when(!type).throwError(ARGUMENT_MUST_BE_PROVIDED); 10 | when(!min).throwError(ARGUMENT_MUST_BE_PROVIDED); 11 | const minInNumber = Number(min); 12 | when(Number.isNaN(minInNumber)).throwError(ARGUMENT_MUST_BE_A_NUMBER); 13 | 14 | if (type === 'number') { 15 | return minForNumber(value, minInNumber); 16 | } else { 17 | return minForString(value, minInNumber); 18 | } 19 | } 20 | 21 | function minForNumber(value: string, min: number) { 22 | const valueInNumber = Number(value); 23 | if (value !== '' && !Number.isNaN(valueInNumber) && valueInNumber >= min) { 24 | return true; 25 | } 26 | 27 | return new RuleError(GREATER_EQUAL, String(min)); 28 | } 29 | 30 | function minForString(value: string, min: number) { 31 | when(min < 0).throwError(ARGUMENT_MUST_BE_POSITIVE); 32 | 33 | if (value.length >= min) { 34 | return true; 35 | } 36 | 37 | return new RuleError(MIN_LENGTH, String(min)); 38 | } 39 | 40 | export default min as Rule; 41 | -------------------------------------------------------------------------------- /src/rules/num-dash.ts: -------------------------------------------------------------------------------- 1 | import { RuleError } from '@/modules/rule-error'; 2 | import { Rule } from '@/types'; 3 | import { numDash as numDashRegex } from '@/utils/regex'; 4 | import { NUM_DASH } from '@/types/rules'; 5 | 6 | function numDash(value: string): true | RuleError { 7 | return numDashRegex.test(value) || new RuleError(NUM_DASH); 8 | } 9 | 10 | export default numDash as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/number.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { number as numberRegex } from '@/utils/regex'; 4 | import { NUMBER } from '@/types/rules'; 5 | 6 | function number(value: string): true | RuleError { 7 | return numberRegex.test(value) || new RuleError(NUMBER); 8 | } 9 | 10 | export default number as Rule; 11 | -------------------------------------------------------------------------------- /src/rules/regex.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when } from '@/utils/helpers'; 4 | import { ARGUMENT_MUST_BE_PROVIDED, INVALID_PATTERN } from '@/types/error-dev'; 5 | import { REGEX } from '@/types/rules'; 6 | 7 | const isValidPattern = (pattern: string) => { 8 | try { 9 | new RegExp(pattern); 10 | return true; 11 | } catch { 12 | return false; 13 | } 14 | }; 15 | 16 | const stringToRegex = (str: string) => { 17 | // Main regex 18 | const main = str.match(/\/(.+)\/.*/)?.[1] ?? ''; 19 | 20 | // Regex options 21 | const options = str.match(/\/.+\/(.*)/)?.[1] ?? ''; 22 | 23 | // Compiled regex 24 | return new RegExp(main, options); 25 | }; 26 | 27 | function regex(value: string, pattern: string): true | RuleError { 28 | when(!pattern).throwError(ARGUMENT_MUST_BE_PROVIDED); 29 | when(isValidPattern(pattern) === false).throwError(INVALID_PATTERN); 30 | 31 | const regExp = stringToRegex(pattern); 32 | 33 | return regExp.test(value) || new RuleError(REGEX); 34 | } 35 | 36 | export default regex as Rule; 37 | -------------------------------------------------------------------------------- /src/rules/required-if.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { required } from '@/rules'; 4 | 5 | function requiredIf(value: string, targetValue = ''): true | RuleError { 6 | const isTargetValueProvided = required(targetValue); 7 | 8 | if (isTargetValueProvided === true) { 9 | return required(value); 10 | } 11 | 12 | return true; 13 | } 14 | 15 | export default requiredIf as Rule; 16 | -------------------------------------------------------------------------------- /src/rules/required.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { REQUIRED } from '@/types/rules'; 4 | 5 | function required(value: string): true | RuleError { 6 | return value.trim().length > 0 || new RuleError(REQUIRED); 7 | } 8 | 9 | export default required as Rule; 10 | -------------------------------------------------------------------------------- /src/rules/size.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when, processArgs } from '@/utils/helpers'; 4 | import { ARGUMENT_MUST_BE_A_NUMBER, ARGUMENT_MUST_BE_POSITIVE, ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 5 | import { EQUAL_LENGTH, EQUAL_NUMBER } from '@/types/rules'; 6 | 7 | function size(value: string, args = ''): true | RuleError { 8 | const [type, size] = processArgs(args); 9 | when(!type).throwError(ARGUMENT_MUST_BE_PROVIDED); 10 | when(!size).throwError(ARGUMENT_MUST_BE_PROVIDED); 11 | 12 | const sizeInNumber = Number(size); 13 | when(Number.isNaN(sizeInNumber)).throwError(ARGUMENT_MUST_BE_A_NUMBER); 14 | 15 | return type === 'number' ? sizeForNumber(value, sizeInNumber) : sizeForString(value, sizeInNumber); 16 | } 17 | 18 | function sizeForNumber(value: string, size: number) { 19 | const valueInNumber = Number(value); 20 | if (value !== '' && !Number.isNaN(valueInNumber) && valueInNumber === size) { 21 | return true; 22 | } 23 | 24 | return new RuleError(EQUAL_NUMBER, String(size)); 25 | } 26 | 27 | function sizeForString(value: string, size: number) { 28 | when(size < 0).throwError(ARGUMENT_MUST_BE_POSITIVE); 29 | 30 | if (value.length === size) { 31 | return true; 32 | } 33 | 34 | return new RuleError(EQUAL_LENGTH, String(size)); 35 | } 36 | 37 | export default size as Rule; 38 | -------------------------------------------------------------------------------- /src/rules/starts-with.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when } from '@/utils/helpers'; 4 | import { STARTS_WITH } from '@/types/rules'; 5 | import { ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 6 | 7 | function startsWith(value: string, start = ''): true | RuleError { 8 | when(start === '').throwError(ARGUMENT_MUST_BE_PROVIDED); 9 | 10 | return value.startsWith(start) || new RuleError(STARTS_WITH, start); 11 | } 12 | 13 | export default startsWith as Rule; 14 | -------------------------------------------------------------------------------- /src/rules/within.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@/types'; 2 | import { RuleError } from '@/modules/rule-error'; 3 | import { when, processArgs } from '@/utils/helpers'; 4 | import { WITHIN } from '@/types/rules'; 5 | import { ARGUMENT_MUST_BE_PROVIDED } from '@/types/error-dev'; 6 | 7 | function within(value: string, values: string): true | RuleError { 8 | const [type, ...list] = processArgs(values); 9 | when(!type).throwError(ARGUMENT_MUST_BE_PROVIDED); 10 | 11 | if (type === 'array') { 12 | const splittedValue = processArgs(value); 13 | for (const value of splittedValue) { 14 | if (!list.includes(value)) { 15 | return new RuleError(WITHIN); 16 | } 17 | } 18 | 19 | return true; 20 | } 21 | 22 | return list.includes(value) || new RuleError(WITHIN); 23 | } 24 | 25 | export default within as Rule; 26 | -------------------------------------------------------------------------------- /src/types/elements.ts: -------------------------------------------------------------------------------- 1 | export const TYPE_CHECKBOX = 'checkbox'; 2 | export const TYPE_FILE = 'file'; 3 | export const TYPE_RADIO = 'radio'; 4 | -------------------------------------------------------------------------------- /src/types/error-dev.ts: -------------------------------------------------------------------------------- 1 | export const ARGUMENT_MUST_BE_PROVIDED = 'An argument must be provided'; 2 | export const ARGUMENT_MUST_BE_A_NUMBER = 'The argument must be a number'; 3 | export const ARGUMENT_MUST_BE_POSITIVE = 'The argument must be a positive number'; 4 | export const ARGUMENT_MUST_BE_AN_INTEGER = 'The argument must be an integer'; 5 | export const INVALID_ARGUMENTS = 'Invalid arguments provided'; 6 | export const INVALID_PATTERN = 'Invalid pattern provided'; 7 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { RuleError } from '@/modules/rule-error'; 2 | import * as rules from '@/types/rules'; 3 | 4 | export type ArrayOfValues = { 5 | [P in keyof T]: T[P][]; 6 | }; 7 | 8 | type ErrorCause = typeof rules; 9 | type FieldArgTypes = string | number | RegExp; 10 | 11 | export type RichXRule = { errorMessage?: string | ((field: FormInputElement) => string); value: FieldArgTypes }; 12 | export type LangKeys = ErrorCause[keyof ErrorCause]; 13 | export type Lang = Partial>; 14 | export type RuleName = typeof rules[keyof typeof rules]; 15 | export type XRules = Record; 16 | export type RuleKey = keyof typeof rules; 17 | 18 | export interface ValidatorOptions { 19 | lang?: Lang; 20 | on?: Partial; 21 | renderErrors?: boolean; 22 | xRules?: XRules; 23 | onFieldChangeValidation?: boolean; 24 | onFieldChangeValidationDelay?: number; 25 | } 26 | 27 | export interface Rule { 28 | (value: string, args?: string): true | RuleError; 29 | } 30 | 31 | export interface ErrorDetail { 32 | element: FormInputElement; 33 | rule: string; 34 | cause: string; 35 | message: string; 36 | args: string[]; 37 | } 38 | 39 | export interface Events { 40 | 'validation:start': (container: HTMLElement) => void; 41 | 'validation:end': (container: HTMLElement, isSuccessful: boolean) => void; 42 | 'validation:success': (container: HTMLElement) => void; 43 | 'validation:failed': (container: HTMLElement) => void; 44 | 'field:error': (container: HTMLElement, element: FormInputElement, errors: ErrorDetail[]) => void; 45 | } 46 | 47 | export type EventsName = keyof Events; 48 | export type EventsOption = Partial; 49 | export type EventsList = { 50 | [P in EventsName]?: Events[P][]; 51 | }; 52 | 53 | export type AdapterFn = ( 54 | rule: string, 55 | rules: string[], 56 | field: FormInputElement, 57 | container: HTMLElement, 58 | xRules?: XRules 59 | ) => string; 60 | 61 | export type FormInputElement = HTMLInputElement | HTMLSelectElement; 62 | -------------------------------------------------------------------------------- /src/types/rules.ts: -------------------------------------------------------------------------------- 1 | export const ACCEPTED = 'accepted'; 2 | export const ALPHA = 'alpha'; 3 | export const ALPHA_NUM = 'alpha-num'; 4 | export const ALPHA_NUM_DASH = 'alpha-num-dash'; 5 | export const BETWEEN_LENGTH = 'between-length'; 6 | export const BETWEEN_NUMBER = 'between-number'; 7 | export const DIGITS = 'digits'; 8 | export const EMAIL = 'email'; 9 | export const ENDS_WITH = 'ends-with'; 10 | export const EQUAL_LENGTH = 'equal-length'; 11 | export const EQUAL_NUMBER = 'equal-number'; 12 | export const GREATER_EQUAL = 'greater-equal'; 13 | export const INTEGER = 'integer'; 14 | export const LESS_EQUAL = 'less-equal'; 15 | export const MAX_LENGTH = 'max-length'; 16 | export const MIN_LENGTH = 'min-length'; 17 | export const NUM_DASH = 'num-dash'; 18 | export const NUMBER = 'number'; 19 | export const REGEX = 'regex'; 20 | export const REQUIRED = 'required'; 21 | export const STARTS_WITH = 'starts-with'; 22 | export const WITHIN = 'within'; 23 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import EventBus from '@/modules/events'; 2 | import Language from '@/modules/language'; 3 | import { LangKeys, FormInputElement, XRules, RichXRule } from '@/types'; 4 | import { TYPE_CHECKBOX, TYPE_RADIO } from '@/types/elements'; 5 | 6 | export function toCamelCase(value: string) { 7 | return value.replace(/-./g, (match) => match[1].toUpperCase()); 8 | } 9 | 10 | export function getValue(element: FormInputElement) { 11 | if (element instanceof HTMLInputElement) { 12 | if (element.type === TYPE_CHECKBOX || element.type === TYPE_RADIO) { 13 | return element.checked ? 'checked' : ''; 14 | } 15 | 16 | return element.value; 17 | } 18 | 19 | if (element instanceof HTMLTextAreaElement) { 20 | return element.value; 21 | } 22 | 23 | if (element instanceof HTMLSelectElement) { 24 | return Array.from(element.selectedOptions) 25 | .map((option) => option.value) 26 | .join(','); 27 | } 28 | 29 | return ''; 30 | } 31 | 32 | export function format(message: string, ...toReplace: string[]) { 33 | return message.replace(/\$(\d)/g, (_, index) => toReplace?.[index - 1] || ''); 34 | } 35 | 36 | export function processRule(rule: string, xRules?: XRules) { 37 | let [name, argsValue = ''] = rule.split(':'); 38 | let customErrorMessage: RichXRule['errorMessage'] = ''; 39 | 40 | if (isXRule(rule)) { 41 | if (!hasArgument(rule)) { 42 | throw new Error(`${rule}: x-rules require an argument that is defined in the config.xRules object`); 43 | } 44 | 45 | name = name.substring(2); 46 | 47 | if (isObject(xRules?.[argsValue])) { 48 | const rule = xRules?.[argsValue] as RichXRule; 49 | customErrorMessage = rule.errorMessage || ''; 50 | argsValue = String(rule.value); 51 | } else { 52 | argsValue = String(xRules?.[argsValue]) || ''; 53 | } 54 | } 55 | 56 | return { 57 | name, 58 | argsValue, 59 | args: processArgs(argsValue), 60 | customErrorMessage, 61 | }; 62 | } 63 | 64 | export function processArgs(args: string) { 65 | return args ? args.split(',') : []; 66 | } 67 | 68 | export function lang(key: string, ...args: string[]) { 69 | const languages = Language.get(); 70 | let item = key; 71 | 72 | if (Object.prototype.hasOwnProperty.call(languages, key)) { 73 | item = languages[key as LangKeys] as string; 74 | } 75 | 76 | return format(item, ...args); 77 | } 78 | 79 | export function when(condition: boolean) { 80 | return { 81 | throwError(message: string) { 82 | if (condition) { 83 | throw new Error(message); 84 | } 85 | }, 86 | }; 87 | } 88 | 89 | export function defaultErrorListeners(events: EventBus) { 90 | events.on('field:error', (_parentEl, element, errors) => { 91 | errors.reverse().forEach((error) => { 92 | const messageElement = document.createElement('p'); 93 | messageElement.classList.add('validator-err'); 94 | messageElement.innerHTML = error.message; 95 | 96 | if (element.parentNode) { 97 | element.parentNode.insertBefore(messageElement, element.nextSibling); 98 | } 99 | }); 100 | }); 101 | 102 | events.on('validation:start', (container) => { 103 | container.querySelectorAll('.validator-err').forEach((el) => { 104 | el.remove(); 105 | }); 106 | }); 107 | } 108 | 109 | export function hasArgument(rule: string) { 110 | return rule.includes(':') && rule.split(':').length === 2; 111 | } 112 | 113 | export function isXRule(rule: string) { 114 | return rule.startsWith('x-'); 115 | } 116 | 117 | export function isObject(obj: unknown) { 118 | return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype; 119 | } 120 | -------------------------------------------------------------------------------- /src/utils/regex.ts: -------------------------------------------------------------------------------- 1 | export const email = 2 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 3 | 4 | export const integer = /^[+-]?\d+$/; 5 | export const number = /^[+-]?(\d+|\d*\.\d*)$/; 6 | 7 | export const alpha = /^[\p{L}\p{M}]+$/u; 8 | export const alphaNum = /^[\p{L}\p{M}\p{N}]+$/u; 9 | export const alphaNumDash = /^[\p{L}\p{M}\p{N}_-]+$/u; 10 | export const numDash = /^[\p{N}_-]+$/u; 11 | export const date = /^\d{4}-\d{2}-\d{2}$/; 12 | -------------------------------------------------------------------------------- /tests/rules/accepted.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { describe, expect, it } from 'vitest'; 3 | import { accepted } from '@/rules'; 4 | 5 | describe('rules: accepted', () => { 6 | it('should accept if checkbox is checked', () => { 7 | expect(accepted('checked')).toBe(true); 8 | }); 9 | 10 | it('should reject if not checkbox is not checked', () => { 11 | expect(accepted('misc')).toBeInstanceOf(Error); 12 | expect(accepted('false')).toBeInstanceOf(Error); 13 | // @ts-ignore 14 | expect(accepted()).toBeInstanceOf(Error); 15 | // @ts-ignore 16 | expect(accepted(true)).toBeInstanceOf(Error); 17 | // @ts-ignore 18 | expect(accepted(false)).toBeInstanceOf(Error); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/rules/alpha-num-dash.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { alphaNumDash } from '@/rules'; 3 | 4 | describe('rules: alpha-dash', () => { 5 | it('Should accept for alpha-numeric characters, dashes, and underscores', () => { 6 | expect(alphaNumDash('hey')).toBe(true); 7 | expect(alphaNumDash('سلام')).toBe(true); 8 | expect(alphaNumDash('嘿')).toBe(true); 9 | expect(alphaNumDash('Füße')).toBe(true); 10 | expect(alphaNumDash('a_a')).toBe(true); 11 | expect(alphaNumDash('a-a')).toBe(true); 12 | expect(alphaNumDash('1')).toBe(true); 13 | expect(alphaNumDash('۱')).toBe(true); 14 | expect(alphaNumDash('٤')).toBe(true); // Arabic 4 15 | expect(alphaNumDash('۴')).toBe(true); // Persian 4 16 | expect(alphaNumDash('a1')).toBe(true); 17 | expect(alphaNumDash('__1')).toBe(true); 18 | expect(alphaNumDash('ـ')).toBe(true); // ARABIC TATWEEL {kashida} 19 | }); 20 | 21 | it('Should reject for non alpha-numeric characters', () => { 22 | expect(alphaNumDash('a a')).toBeInstanceOf(Error); 23 | expect(alphaNumDash('a b_1')).toBeInstanceOf(Error); 24 | expect(alphaNumDash('😉')).toBeInstanceOf(Error); 25 | expect(alphaNumDash('(e)')).toBeInstanceOf(Error); 26 | expect(alphaNumDash(' ')).toBeInstanceOf(Error); 27 | expect(alphaNumDash('—')).toBeInstanceOf(Error); // EM DASH 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/rules/alpha-num.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { alphaNum } from '@/rules'; 3 | 4 | describe('rules: alpha-num', () => { 5 | it('should accept for alpha-numeric characters', () => { 6 | expect(alphaNum('hey')).toBe(true); 7 | expect(alphaNum('سلام')).toBe(true); 8 | expect(alphaNum('嘿')).toBe(true); 9 | expect(alphaNum('Füße')).toBe(true); 10 | expect(alphaNum('a1')).toBe(true); 11 | expect(alphaNum('1')).toBe(true); 12 | expect(alphaNum('۱')).toBe(true); 13 | expect(alphaNum('٤')).toBe(true); // Arabic 4 14 | expect(alphaNum('۴')).toBe(true); // Persian 4 15 | expect(alphaNum('a1')).toBe(true); 16 | }); 17 | 18 | it('should reject for non-alpha-numeric characters', () => { 19 | expect(alphaNum('a a')).toBeInstanceOf(Error); 20 | expect(alphaNum('a ')).toBeInstanceOf(Error); 21 | expect(alphaNum('a-a')).toBeInstanceOf(Error); 22 | expect(alphaNum('a_a')).toBeInstanceOf(Error); 23 | expect(alphaNum('a-1')).toBeInstanceOf(Error); 24 | expect(alphaNum('a 1')).toBeInstanceOf(Error); 25 | expect(alphaNum('😉')).toBeInstanceOf(Error); 26 | expect(alphaNum('(e)')).toBeInstanceOf(Error); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/rules/alpha.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { alpha } from '@/rules'; 3 | 4 | describe('rules: alpha', () => { 5 | it('should accept for alphabetic characters', () => { 6 | expect(alpha('hey')).toBe(true); 7 | expect(alpha('سلام')).toBe(true); 8 | expect(alpha('嘿')).toBe(true); 9 | expect(alpha('Füße')).toBe(true); 10 | }); 11 | 12 | it('should reject for non-alphabetic characters', () => { 13 | expect(alpha('a a')).toBeInstanceOf(Error); 14 | expect(alpha('a ')).toBeInstanceOf(Error); 15 | expect(alpha('a-a')).toBeInstanceOf(Error); 16 | expect(alpha('a_a')).toBeInstanceOf(Error); 17 | expect(alpha('a1')).toBeInstanceOf(Error); 18 | expect(alpha('1')).toBeInstanceOf(Error); 19 | expect(alpha('😉')).toBeInstanceOf(Error); 20 | expect(alpha('(e)')).toBeInstanceOf(Error); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/rules/between.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { between } from '@/rules'; 3 | 4 | describe('rules: between', () => { 5 | it('should accept value with correct input (string type)', () => { 6 | expect(between('text', 'string,0,4')).toBe(true); 7 | expect(between('text', 'string,4,10')).toBe(true); 8 | expect(between('text', 'string,0,10')).toBe(true); 9 | 10 | expect(between('text with space', 'string,0,15')).toBe(true); 11 | expect(between('text with space', 'string,15,20')).toBe(true); 12 | expect(between('text with space', 'string,0,20')).toBe(true); 13 | }); 14 | 15 | it('should accept value with correct input (number type)', () => { 16 | expect(between('100', 'number,0,100')).toBe(true); 17 | expect(between('100', 'number,100,200')).toBe(true); 18 | expect(between('100', 'number,0,200')).toBe(true); 19 | 20 | expect(between('-100', 'number,-200,-100')).toBe(true); 21 | expect(between('-100', 'number,-100,0')).toBe(true); 22 | expect(between('-100', 'number,-200,0')).toBe(true); 23 | }); 24 | 25 | it('should reject value with incorrect input (string type)', () => { 26 | expect(between('text', 'string,5,10')).instanceOf(Error); 27 | expect(between('text', 'string,0,3')).instanceOf(Error); 28 | 29 | expect(between('text with space', 'string,16,20')).instanceOf(Error); 30 | expect(between('text with space', 'string,10,14')).instanceOf(Error); 31 | }); 32 | 33 | it('should reject value with incorrect input (number type)', () => { 34 | expect(between('100', 'number,101,200')).instanceOf(Error); 35 | expect(between('100', 'number,0,99')).instanceOf(Error); 36 | 37 | expect(between('-100', 'number,-200,-101')).instanceOf(Error); 38 | expect(between('-100', 'number,-99,0')).instanceOf(Error); 39 | }); 40 | 41 | it('should throw error on invalid argument', () => { 42 | expect(() => between('text')).toThrowError(); 43 | expect(() => between('text', '')).toThrowError(); 44 | expect(() => between('text', 'string')).toThrowError(); 45 | expect(() => between('text', '5')).toThrowError(); 46 | expect(() => between('text', '0,1')).toThrowError(); 47 | 48 | expect(() => between('text', 'string,1')).toThrowError(); 49 | expect(() => between('text', 'string,1,1')).toThrowError(); 50 | expect(() => between('text', 'string,-1,-1')).toThrowError(); 51 | expect(() => between('text', 'string,-1,0')).toThrowError(); 52 | expect(() => between('text', 'string,0,-1')).toThrowError(); 53 | expect(() => between('text', 'string,10,5')).toThrowError(); 54 | expect(() => between('text', 'string,-5,-10')).toThrowError(); 55 | expect(() => between('text', 'string,0,text')).toThrowError(); 56 | expect(() => between('text', 'string,text,0')).toThrowError(); 57 | expect(() => between('text', 'string,text,text')).toThrowError(); 58 | 59 | expect(() => between('text', 'number,1,1')).toThrowError(); 60 | expect(() => between('text', 'number,-1,-1')).toThrowError(); 61 | expect(() => between('text', 'number,10,5')).toThrowError(); 62 | expect(() => between('text', 'number,-5,-10')).toThrowError(); 63 | expect(() => between('text', 'number,text,text')).toThrowError(); 64 | expect(() => between('text', 'number,0,text')).toThrowError(); 65 | expect(() => between('text', 'number,text,0')).toThrowError(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/rules/digits.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { digits } from '@/rules'; 3 | 4 | describe('rules: digits', () => { 5 | it('should be accepted with correct value and argument', () => { 6 | expect(digits('0000000000', '10')).toBe(true); 7 | expect(digits('1000000000', '10')).toBe(true); 8 | expect(digits('-1000000000', '10')).toBe(true); 9 | 10 | expect(digits('1000000000000000000000000', '25')).toBe(true); 11 | expect(digits('-999999999999999999999999999999', '30')).toBe(true); 12 | }); 13 | 14 | it('should not be accepted with an unexpected value', () => { 15 | const length = '10'; 16 | 17 | expect(digits((100_000_000).toString(), length)).toBeInstanceOf(Error); 18 | expect(digits('1234567890.1', length)).toBeInstanceOf(Error); 19 | }); 20 | 21 | it('should throw an error with incorrect argument', () => { 22 | const value = (100_000_000).toString(); 23 | 24 | expect(() => digits(value)).toThrowError(); 25 | expect(() => digits(value, '')).toThrowError(); 26 | expect(() => digits(value, '10.4')).toThrowError(); 27 | expect(() => digits(value, '-1')).toThrowError(); 28 | expect(() => digits(value, '0')).toThrowError(); 29 | expect(() => digits(value, 'yay')).toThrowError(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/rules/email.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { email } from '@/rules'; 3 | 4 | describe('rules: email', () => { 5 | it('should accept email', () => { 6 | expect(email('test@test.com')).toBe(true); 7 | expect(email('test123@gmail.com')).toBe(true); 8 | expect(email('test.123@iran.ir')).toBe(true); 9 | expect(email('123.test.123@yahooo.com')).toBe(true); 10 | }); 11 | 12 | it('should reject non-email', () => { 13 | expect(email('test.com')).toBeInstanceOf(Error); 14 | expect(email('test')).toBeInstanceOf(Error); 15 | expect(email('test@test')).toBeInstanceOf(Error); 16 | expect(email('test@test.')).toBeInstanceOf(Error); 17 | expect(email('@test')).toBeInstanceOf(Error); 18 | expect(email('@test.')).toBeInstanceOf(Error); 19 | expect(email('@test.com')).toBeInstanceOf(Error); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/rules/ends-with.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { endsWith } from '@/rules'; 3 | 4 | describe('rules: ends-with', () => { 5 | it('should accept value with correct input', () => { 6 | expect(endsWith('abc', 'abc')).toBe(true); 7 | expect(endsWith('abcdef', 'def')).toBe(true); 8 | }); 9 | 10 | it('should reject value with incorrect input', () => { 11 | expect(endsWith('abc', 'abcdef')).instanceOf(Error); 12 | expect(endsWith('def', 'abcdef')).instanceOf(Error); 13 | expect(endsWith('abc ', ' abc')).instanceOf(Error); 14 | expect(endsWith('abc', 'abc ')).instanceOf(Error); 15 | }); 16 | 17 | it('should throw error on invalid argument', () => { 18 | expect(() => endsWith('abc')).toThrowError(); 19 | expect(() => endsWith('abc', '')).toThrowError(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/rules/int.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { int } from '@/rules'; 3 | 4 | describe('rules: int', () => { 5 | it('should accept integers', () => { 6 | expect(int('123')).toBe(true); 7 | expect(int('-123')).toBe(true); 8 | expect(int('+123')).toBe(true); 9 | expect(int('0')).toBe(true); 10 | expect(int('10')).toBe(true); 11 | expect(int('-10')).toBe(true); 12 | expect(int('+0')).toBe(true); 13 | expect(int('-0')).toBe(true); 14 | }); 15 | 16 | it('should reject non-integers', () => { 17 | expect(int('123a')).toBeInstanceOf(Error); 18 | expect(int('a123 ')).toBeInstanceOf(Error); 19 | expect(int('-a')).toBeInstanceOf(Error); 20 | expect(int('+a')).toBeInstanceOf(Error); 21 | expect(int('123a')).toBeInstanceOf(Error); 22 | expect(int('1.2')).toBeInstanceOf(Error); 23 | expect(int('-1.2')).toBeInstanceOf(Error); 24 | expect(int('e')).toBeInstanceOf(Error); 25 | expect(int('0.5')).toBeInstanceOf(Error); 26 | expect(int('-0.5')).toBeInstanceOf(Error); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/rules/max.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { max } from '@/rules'; 3 | 4 | describe('rules: max', () => { 5 | it('should accept value with correct input (string type)', () => { 6 | expect(max('abc', 'string,4')).toBe(true); 7 | expect(max('abc', 'string,5')).toBe(true); 8 | expect(max('another', 'string,10')).toBe(true); 9 | expect(max('equal', 'string,5')).toBe(true); 10 | expect(max('text with space', 'string,15')).toBe(true); 11 | }); 12 | 13 | it('should accept value with correct input (number type)', () => { 14 | expect(max('150', 'number,150')).toBe(true); 15 | expect(max('100', 'number,150')).toBe(true); 16 | expect(max('-100', 'number,0')).toBe(true); 17 | expect(max('-10', 'number,-10')).toBe(true); 18 | }); 19 | 20 | it('should reject value with incorrect input (string type)', () => { 21 | expect(max('abc', 'string,2')).instanceOf(Error); 22 | expect(max('abc', 'string,1')).instanceOf(Error); 23 | expect(max('another', 'string,6')).instanceOf(Error); 24 | expect(max('equal', 'string,4')).instanceOf(Error); 25 | expect(max('text with space', 'string,13')).instanceOf(Error); 26 | }); 27 | 28 | it('should reject value with incorrect input (number type)', () => { 29 | expect(max('100', 'number,99')).instanceOf(Error); 30 | expect(max('50', 'number,0')).instanceOf(Error); 31 | expect(max('1', 'number,0')).instanceOf(Error); 32 | expect(max('-200', 'number,-1000')).instanceOf(Error); 33 | expect(max('0', 'number,-1')).instanceOf(Error); 34 | expect(max('', 'number,10')).instanceOf(Error); 35 | expect(max('', 'number,-10')).instanceOf(Error); 36 | }); 37 | 38 | it('should throw error on invalid argument', () => { 39 | expect(() => max('1')).toThrowError(); 40 | expect(() => max('1', '')).toThrowError(); 41 | expect(() => max('1', '3')).toThrowError(); 42 | expect(() => max('1', '-1')).toThrowError(); 43 | 44 | expect(() => max('1', 'string')).toThrowError(); 45 | expect(() => max('1', 'string,-1')).toThrowError(); 46 | expect(() => max('1', 'string,text')).toThrowError(); 47 | 48 | expect(() => max('1', 'number')).toThrowError(); 49 | expect(() => max('1', 'number,text')).toThrowError(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/rules/min.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { min } from '@/rules'; 3 | 4 | describe('rules: min', () => { 5 | it('should accept value with correct input (string type)', () => { 6 | expect(min('abc', 'string,2')).toBe(true); 7 | expect(min('abc', 'string,3')).toBe(true); 8 | expect(min('another', 'string,6')).toBe(true); 9 | expect(min('equal', 'string,5')).toBe(true); 10 | expect(min('text with space', 'string,15')).toBe(true); 11 | }); 12 | 13 | it('should accept value with correct input (number type)', () => { 14 | expect(min('150', 'number,150')).toBe(true); 15 | expect(min('100', 'number,50')).toBe(true); 16 | expect(min('-100', 'number,-200')).toBe(true); 17 | expect(min('-10', 'number,-10')).toBe(true); 18 | }); 19 | 20 | it('should reject value with incorrect input (string type)', () => { 21 | expect(min('abc', 'string,4')).instanceOf(Error); 22 | expect(min('abc', 'string,5')).instanceOf(Error); 23 | expect(min('another', 'string,8')).instanceOf(Error); 24 | expect(min('equal', 'string,6')).instanceOf(Error); 25 | expect(min('text with space', 'string,17')).instanceOf(Error); 26 | }); 27 | 28 | it('should reject value with incorrect input (number type)', () => { 29 | expect(min('100', 'number,101')).instanceOf(Error); 30 | expect(min('50', 'number,100')).instanceOf(Error); 31 | expect(min('1', 'number,2')).instanceOf(Error); 32 | expect(min('-200', 'number,-100')).instanceOf(Error); 33 | expect(min('0', 'number,1')).instanceOf(Error); 34 | expect(min('', 'number,10')).instanceOf(Error); 35 | expect(min('', 'number,-10')).instanceOf(Error); 36 | }); 37 | 38 | it('should throw error on invalid argument', () => { 39 | expect(() => min('1')).toThrowError(); 40 | expect(() => min('1', '')).toThrowError(); 41 | expect(() => min('1', '3')).toThrowError(); 42 | expect(() => min('1', '-1')).toThrowError(); 43 | 44 | expect(() => min('1', 'string')).toThrowError(); 45 | expect(() => min('1', 'string,-1')).toThrowError(); 46 | expect(() => min('1', 'string,text')).toThrowError(); 47 | 48 | expect(() => min('1', 'number')).toThrowError(); 49 | expect(() => min('1', 'number,text')).toThrowError(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/rules/num-dash.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { numDash } from '@/rules'; 3 | 4 | describe('rules: num-dash', () => { 5 | it('should accept for numbers with dashes', () => { 6 | expect(numDash('1')).toBe(true); 7 | expect(numDash('1-2-3')).toBe(true); 8 | expect(numDash('1-2-3-4-')).toBe(true); 9 | expect(numDash('1_2')).toBe(true); // Underscores are accpeted 10 | expect(numDash('۱')).toBe(true); 11 | expect(numDash('٤')).toBe(true); // Arabic 4 12 | expect(numDash('۴')).toBe(true); // Persian 4 13 | expect(numDash('۴-۴-۴-۴')).toBe(true); // Persian 4 14 | }); 15 | 16 | it('should reject for non-alpha-numeric characters', () => { 17 | expect(numDash('hey')).toBeInstanceOf(Error); 18 | expect(numDash('سلام')).toBeInstanceOf(Error); 19 | expect(numDash('嘿')).toBeInstanceOf(Error); 20 | expect(numDash('Füße')).toBeInstanceOf(Error); 21 | expect(numDash('a1')).toBeInstanceOf(Error); 22 | expect(numDash('a a')).toBeInstanceOf(Error); 23 | expect(numDash('a ')).toBeInstanceOf(Error); 24 | expect(numDash('a-a')).toBeInstanceOf(Error); 25 | expect(numDash('a_a')).toBeInstanceOf(Error); 26 | expect(numDash('a-1')).toBeInstanceOf(Error); 27 | expect(numDash('a 1')).toBeInstanceOf(Error); 28 | expect(numDash('😉')).toBeInstanceOf(Error); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/rules/number.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { number } from '@/rules'; 3 | 4 | describe('rules: number', () => { 5 | it('should accept numbers', () => { 6 | expect(number('123')).toBe(true); 7 | expect(number('-123')).toBe(true); 8 | expect(number('+123')).toBe(true); 9 | expect(number('1.5')).toBe(true); 10 | expect(number('+1.5')).toBe(true); 11 | expect(number('-1.5')).toBe(true); 12 | expect(number('0')).toBe(true); 13 | expect(number('10')).toBe(true); 14 | expect(number('-10')).toBe(true); 15 | expect(number('+0')).toBe(true); 16 | expect(number('-0')).toBe(true); 17 | expect(number('0.5')).toBe(true); 18 | expect(number('+0.5')).toBe(true); 19 | expect(number('-0.5')).toBe(true); 20 | expect(number('.5')).toBe(true); 21 | expect(number('+.5')).toBe(true); 22 | expect(number('-.5')).toBe(true); 23 | expect(number('1.')).toBe(true); 24 | expect(number('-1.')).toBe(true); 25 | expect(number('+1.')).toBe(true); 26 | }); 27 | 28 | it('should reject non-numbers', () => { 29 | expect(number('123a')).toBeInstanceOf(Error); 30 | expect(number('a123 ')).toBeInstanceOf(Error); 31 | expect(number('-a')).toBeInstanceOf(Error); 32 | expect(number('+a')).toBeInstanceOf(Error); 33 | expect(number('123a')).toBeInstanceOf(Error); 34 | expect(number('e')).toBeInstanceOf(Error); 35 | expect(number('NaN')).toBeInstanceOf(Error); 36 | expect(number('123.5n')).toBeInstanceOf(Error); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/rules/regex.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { regex } from '@/rules'; 3 | import * as Regexes from '@/utils/regex'; 4 | import { RuleError } from '@/modules/rule-error'; 5 | import { ARGUMENT_MUST_BE_PROVIDED, INVALID_PATTERN } from '@/types/error-dev'; 6 | 7 | describe('rules: regex', () => { 8 | it('should accept if the pattern matches', () => { 9 | // Digits 10 | expect(regex('1', '/^\\d+$/')).toBe(true); 11 | 12 | // Numbers 13 | expect(regex('-.5', Regexes.number.toString())).toBe(true); 14 | 15 | // Decimal numbers 16 | expect(regex('1.5', '/^\\d*\\.\\d+$/')).toBe(true); 17 | 18 | // No digits 19 | expect(regex('abc', '/^\\D+$/')).toBe(true); 20 | 21 | // Email 22 | expect(regex('a@example.com', Regexes.email.toString())).toBe(true); 23 | 24 | // alphaNumDash 25 | expect(regex('abc-123', Regexes.alphaNumDash.toString())).toBe(true); 26 | 27 | // Alphanumeric 28 | expect(regex('abc123', Regexes.alphaNum.toString())).toBe(true); 29 | 30 | // Date 31 | expect(regex('2019-01-01', Regexes.date.toString())).toBe(true); 32 | }); 33 | 34 | it("should reject if pattern doesn't match", () => { 35 | expect(regex('2019-01-1', Regexes.date.toString())).toBeInstanceOf(RuleError); 36 | expect(regex('2019/01/01', Regexes.date.toString())).toBeInstanceOf(RuleError); 37 | 38 | // Decimal numbers 39 | expect(regex('15', '/^\\d*\\.\\d+$/')).toBeInstanceOf(RuleError); 40 | }); 41 | 42 | it('should throw error on invalid argument', () => { 43 | expect(() => regex('...')).toThrowError(ARGUMENT_MUST_BE_PROVIDED); 44 | expect(() => regex('...', '')).toThrowError(ARGUMENT_MUST_BE_PROVIDED); 45 | 46 | // Invalid Patterns 47 | expect(() => regex('...', '^/[a-z')).toThrowError(INVALID_PATTERN); 48 | expect(() => regex('...', '(a')).toThrowError(INVALID_PATTERN); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/rules/required.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { required } from '@/rules'; 3 | 4 | describe('rules: required', () => { 5 | it('should accept if value passed', () => { 6 | expect(required('1')).toBe(true); 7 | expect(required('+')).toBe(true); 8 | expect(required('@')).toBe(true); 9 | expect(required(' test ')).toBe(true); 10 | expect(required(' e ')).toBe(true); 11 | expect(required(' 1')).toBe(true); 12 | expect(required('1 ')).toBe(true); 13 | expect(required('~')).toBe(true); 14 | }); 15 | 16 | it('should reject if value not passed', () => { 17 | expect(required('')).toBeInstanceOf(Error); 18 | expect(required(' ')).toBeInstanceOf(Error); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/rules/size.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { size } from '@/rules'; 3 | 4 | describe('rules: size', () => { 5 | it('should accept value with correct input (string type)', () => { 6 | expect(size('abc', 'string,3')).toBe(true); 7 | expect(size('', 'string,0')).toBe(true); 8 | expect(size('abcdefghijklmnopqrstuvwxyz', 'string,26')).toBe(true); 9 | }); 10 | 11 | it('should accept value with correct input (number type)', () => { 12 | expect(size('123', 'number,123')).toBe(true); 13 | expect(size('0', 'number,0')).toBe(true); 14 | expect(size('123456789', 'number,123456789')).toBe(true); 15 | }); 16 | 17 | it('should reject value with incorrect input (string type)', () => { 18 | expect(size('abc', 'string,0')).instanceOf(Error); 19 | expect(size('abc', 'string,2')).instanceOf(Error); 20 | expect(size('abc', 'string,4')).instanceOf(Error); 21 | expect(size('', 'string,1')).instanceOf(Error); 22 | }); 23 | 24 | it('should reject value with incorrect input (number type)', () => { 25 | expect(size('123', 'number,0')).instanceOf(Error); 26 | expect(size('123', 'number,124')).instanceOf(Error); 27 | expect(size('123', 'number,1234')).instanceOf(Error); 28 | expect(size('123', 'number,-123')).instanceOf(Error); 29 | expect(size('-123', 'number,123')).instanceOf(Error); 30 | }); 31 | 32 | it('should throw error on invalid argument', () => { 33 | expect(() => size('abc')).toThrowError(); 34 | expect(() => size('abc', '')).toThrowError(); 35 | expect(() => size('abc', '3')).toThrowError(); 36 | expect(() => size('abc', '-1')).toThrowError(); 37 | 38 | expect(() => size('abc', 'string')).toThrowError(); 39 | expect(() => size('abc', 'string,-1')).toThrowError(); 40 | expect(() => size('abc', 'string,text')).toThrowError(); 41 | 42 | expect(() => size('abc', 'number')).toThrowError(); 43 | expect(() => size('abc', 'number,text')).toThrowError(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/rules/starts-with.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { startsWith } from '@/rules'; 3 | 4 | describe('rules: starts-with', () => { 5 | it('should accept value with correct input', () => { 6 | expect(startsWith('abc', 'abc')).toBe(true); 7 | expect(startsWith('abcdef', 'abc')).toBe(true); 8 | }); 9 | 10 | it('should reject value with incorrect input', () => { 11 | expect(startsWith('abc', 'abcdef')).instanceOf(Error); 12 | expect(startsWith('abc', ' abc')).instanceOf(Error); 13 | expect(startsWith(' abc', 'abc')).instanceOf(Error); 14 | }); 15 | 16 | it('should throw error on invalid argument', () => { 17 | expect(() => startsWith('abc')).toThrowError(); 18 | expect(() => startsWith('abc', '')).toThrowError(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/rules/within.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { within } from '@/rules'; 3 | 4 | describe('rules: within', () => { 5 | it('should accept value with correct input', () => { 6 | expect(within('abc', 'string,abc')).toBe(true); 7 | expect(within('abc', 'string,abc,def')).toBe(true); 8 | expect(within('def', 'string,abc,def')).toBe(true); 9 | expect(within('def', 'string,abc,def,ghi')).toBe(true); 10 | expect(within('ghi', 'string,abc,def,ghi')).toBe(true); 11 | 12 | expect(within('1', 'array,1,3')).toBe(true); 13 | expect(within('3', 'array,1,3')).toBe(true); 14 | expect(within('1,3', 'array,1,3')).toBe(true); 15 | expect(within('3,1', 'array,1,3')).toBe(true); 16 | expect(within('', 'array,')).toBe(true); 17 | }); 18 | 19 | it('should reject value with incorrect input', () => { 20 | expect(within('abc', 'string,jkl')).instanceOf(Error); 21 | expect(within('abc', 'string,jkl,abcd')).instanceOf(Error); 22 | expect(within('def', 'string,abcdef')).instanceOf(Error); 23 | expect(within('def', 'string,abcdef,jkl')).instanceOf(Error); 24 | expect(within('ghi', 'string,efghijk')).instanceOf(Error); 25 | 26 | expect(within('1,2', 'array,1,3')).instanceOf(Error); 27 | expect(within('2', 'array,1,3')).instanceOf(Error); 28 | }); 29 | 30 | it('should throw error on invalid argument', () => { 31 | expect(() => within('abc')).toThrowError(); 32 | expect(() => within('abc', '')).toThrowError(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/utils/process-args.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { processArgs } from '@/utils/helpers'; 3 | 4 | it('should process args correctly', () => { 5 | expect(processArgs('')).toEqual([]); 6 | expect(processArgs('arg1')).toEqual(['arg1']); 7 | expect(processArgs('arg1,arg2')).toEqual(['arg1', 'arg2']); 8 | expect(processArgs(',arg2')).toEqual(['', 'arg2']); 9 | expect(processArgs('arg1,')).toEqual(['arg1', '']); 10 | expect(processArgs(String(/^([0-9]{5})-([0-9]{5})$/))).toEqual(['/^([0-9]{5})-([0-9]{5})$/']); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/utils/process-rule.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { processRule } from '@/utils/helpers'; 3 | 4 | it('should process "accepted" correctly', () => { 5 | const rule = 'accepted'; 6 | const result = processRule(rule); 7 | 8 | expect(result.name).toBe('accepted'); 9 | expect(result.argsValue).toBe(''); 10 | expect(result.args).toHaveLength(0); 11 | }); 12 | 13 | it('should process "accepted:" correctly', () => { 14 | const rule = 'accepted:'; 15 | const result = processRule(rule); 16 | 17 | expect(result.name).toBe('accepted'); 18 | expect(result.argsValue).toBe(''); 19 | expect(result.args).toHaveLength(0); 20 | }); 21 | 22 | it('should process "accepted:arg1" correctly', () => { 23 | const rule = 'accepted:arg1'; 24 | const result = processRule(rule); 25 | 26 | expect(result.name).toBe('accepted'); 27 | expect(result.argsValue).toBe('arg1'); 28 | expect(result.args).toEqual(['arg1']); 29 | }); 30 | 31 | it('should process "accepted:arg1,arg2" correctly', () => { 32 | const rule = 'accepted:arg1,arg2'; 33 | const result = processRule(rule); 34 | 35 | expect(result.name).toBe('accepted'); 36 | expect(result.argsValue).toBe('arg1,arg2'); 37 | expect(result.args).toEqual(['arg1', 'arg2']); 38 | }); 39 | 40 | it('should process "accepted:,arg2" correctly', () => { 41 | const rule = 'accepted:,arg2'; 42 | const result = processRule(rule); 43 | 44 | expect(result.name).toBe('accepted'); 45 | expect(result.argsValue).toBe(',arg2'); 46 | expect(result.args).toEqual(['', 'arg2']); 47 | }); 48 | 49 | it('should process "accepted:arg1," correctly', () => { 50 | const rule = 'accepted:arg1,'; 51 | const result = processRule(rule); 52 | 53 | expect(result.name).toBe('accepted'); 54 | expect(result.argsValue).toBe('arg1,'); 55 | expect(result.args).toEqual(['arg1', '']); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/utils/to-camel-case.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { toCamelCase } from '@/utils/helpers'; 3 | 4 | it('should convert kebab-case to camelCase', () => { 5 | expect(toCamelCase('kebab-case')).toBe('kebabCase'); 6 | expect(toCamelCase('kebab-case-with-dashes')).toBe('kebabCaseWithDashes'); 7 | expect(toCamelCase('test')).toBe('test'); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | "~/*": ["./playground/*"] 20 | } 21 | }, 22 | "include": ["src", "playground", "tests"] 23 | } 24 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | '@': resolve(__dirname, './src'), 8 | '~': resolve(__dirname, './playground'), 9 | }, 10 | }, 11 | test: { 12 | // ... 13 | }, 14 | }); 15 | --------------------------------------------------------------------------------