├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── tests.js.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── clean-package.js ├── jest.config.cjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── app.css ├── app.html ├── components │ └── Debug.svelte ├── docs │ ├── 1_Install.md │ ├── 2_Examples.md │ ├── 3_Form.md │ ├── 4_Field.md │ ├── 5_Combined.md │ ├── 6_Validators.md │ ├── 7_UseStyle.md │ ├── 8_Migrating.md │ └── 9_Contribute.md ├── global.d.ts ├── lib │ ├── combined.ts │ ├── createFieldStore.ts │ ├── field.ts │ ├── form.ts │ ├── index.ts │ ├── types.ts │ ├── use.style.ts │ └── validators │ │ ├── between.ts │ │ ├── email.ts │ │ ├── index.ts │ │ ├── matchField.ts │ │ ├── max.ts │ │ ├── min.ts │ │ ├── not.ts │ │ ├── pattern.ts │ │ ├── required.ts │ │ ├── url.ts │ │ └── validator.ts ├── routes │ ├── __layout.svelte │ ├── index.svelte │ └── test │ │ ├── __layout.reset.svelte │ │ └── index.svelte ├── tests │ ├── combined.test.ts │ ├── field.test.ts │ ├── form.test.ts │ └── validators │ │ ├── between.test.ts │ │ ├── email.test.ts │ │ ├── matchField.test.ts │ │ ├── max.test.ts │ │ ├── min.test.ts │ │ ├── not.test.ts │ │ ├── pattern.test.ts │ │ ├── required.test.ts │ │ └── url.test.ts └── utils │ └── isPromise.ts ├── static ├── favicon.png └── prism.css ├── svelte.config.js ├── tailwind.config.cjs └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: chainlist 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | assignees: chainlist 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.js.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | pull_request: 7 | branches: [ master, dev ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 16.x] 17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: pnpm/action-setup@646cdf48217256a3d0b80361c5a50727664284f2 22 | with: 23 | version: 6.10.0 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: '14' 27 | cache: 'pnpm' 28 | - run: pnpm install 29 | - run: pnpm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.preferences.importModuleSpecifierEnding": "js", 3 | "typescript.preferences.importModuleSpecifierEnding": "js" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # svelte-forms 2 | 3 | ## 2.3.1 4 | 5 | ### Patch Changes 6 | 7 | - Fix behavior when validate on change is false 8 | 9 | ## 2.3.0 10 | 11 | ### Minor Changes 12 | 13 | - add clear function to field and form 14 | - add summary property to reflect data from binded fields 15 | 16 | ## 2.2.1 17 | 18 | ### Patch Changes 19 | 20 | - Fix badly deployed NPM package 21 | 22 | ## 2.2.0 23 | 24 | ### Minor Changes 25 | 26 | - Added summary method to form object 27 | 28 | ## 2.1.0 29 | 30 | ### Minor Changes 31 | 32 | - Added pattern validator 33 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at dev.chainlist@gmail.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin Guillouard and other contributors 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 | 2 | # svelte-forms [![Tests](https://github.com/chainlist/svelte-forms/actions/workflows/tests.js.yml/badge.svg)](https://github.com/chainlist/svelte-forms/actions/workflows/tests.js.yml) [![Downloads](https://img.shields.io/npm/dt/svelte-forms)](https://www.npmjs.com/package/svelte-forms) 3 | 4 | ## V2 is here! 🎊 5 | 6 | Check out the new documentation website [here](https://chainlist.github.io/svelte-forms/) 7 | 8 | ## todo list 9 | 10 | - Fix the issues when they appear! :) 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript', 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /clean-package.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const documentationPackages = [ 4 | 'prism-svelte', 5 | 'prismjs', 6 | 'svelte-awesome', 7 | 'ua-parser-js', 8 | 'lodash.kebabcase' 9 | ]; 10 | const pckg = JSON.parse(fs.readFileSync('./package/package.json')); 11 | 12 | Object.keys(pckg.dependencies).forEach((dep) => { 13 | if (documentationPackages.includes(dep)) { 14 | delete pckg.dependencies[dep]; 15 | } 16 | }); 17 | 18 | fs.writeFileSync('./package/package.json', JSON.stringify(pckg, null, 2)); 19 | 20 | console.log('Cleaning of package.json done...'); 21 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | transform: { 4 | '^.+\\.ts$': 'ts-jest', 5 | '^.+\\.js$': 'ts-jest' 6 | }, 7 | moduleFileExtensions: ['js', 'ts', 'svelte'], 8 | moduleNameMapper: { 9 | '^\\./(.*)\\.js$': './$1', 10 | '^\\$lib(.*)$': '/src/lib$1', 11 | '^\\$app(.*)$': [ 12 | '/.svelte-kit/dev/runtime/app$1', 13 | '/.svelte-kit/build/runtime/app$1' 14 | ] 15 | }, 16 | collectCoverageFrom: ['src/**/*.{ts,tsx,svelte,js,jsx}'] 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-forms", 3 | "keywords": [ 4 | "svelte", 5 | "forms", 6 | "validations", 7 | "svelte-forms" 8 | ], 9 | "version": "2.3.1", 10 | "scripts": { 11 | "dev": "svelte-kit dev", 12 | "build": "svelte-kit build && touch build/.nojekyll", 13 | "package": "pnpm run test && svelte-kit package && node clean-package.js", 14 | "preview": "svelte-kit preview", 15 | "check": "svelte-check --tsconfig ./tsconfig.json", 16 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 17 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. .", 18 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .", 19 | "test": "jest --runInBand", 20 | "test:watch": "jest --watch", 21 | "test:coverage": "jest --coverage", 22 | "deploy:docs": "pnpm run test && pnpm run build && npx gh-pages -d build -t true", 23 | "deploy:package": "pnpm run package && cd package && npm publish", 24 | "deploy": "pnpm run deploy:docs && pnpm run deploy:package" 25 | }, 26 | "devDependencies": { 27 | "@babel/preset-env": "^7.16.7", 28 | "@changesets/cli": "^2.20.0", 29 | "@sveltejs/adapter-auto": "next", 30 | "@sveltejs/adapter-static": "^1.0.0-next.24", 31 | "@sveltejs/kit": "next", 32 | "@testing-library/jest-dom": "^5.16.1", 33 | "@testing-library/svelte": "^3.0.3", 34 | "@types/jest": "^27.4.0", 35 | "autoprefixer": "^10.4.2", 36 | "cssnano": "^5.0.15", 37 | "jest": "^27.4.7", 38 | "jest-in-case": "^1.0.2", 39 | "jest-mock-promise": "^1.1.12", 40 | "markdown-it": "^12.3.1", 41 | "markdown-it-anchor": "^8.4.1", 42 | "node-sass": "^6.0.1", 43 | "postcss": "^8.4.5", 44 | "postcss-load-config": "^3.1.1", 45 | "prettier": "^2.5.1", 46 | "prettier-plugin-svelte": "^2.5.1", 47 | "svelte": "^3.45.0", 48 | "svelte-check": "^2.2.11", 49 | "svelte-jester": "^2.1.5", 50 | "svelte-preprocess": "^4.10.1", 51 | "svelte2tsx": "^0.4.12", 52 | "tailwindcss": "^2.2.19", 53 | "ts-jest": "^27.1.2", 54 | "tslib": "^2.3.1", 55 | "typescript": "^4.5.4", 56 | "vite-plugin-markdown": "^2.0.2" 57 | }, 58 | "type": "module", 59 | "dependencies": { 60 | "is-promise": "^4.0.0", 61 | "lodash.kebabcase": "^4.1.1", 62 | "prism-svelte": "^0.4.7", 63 | "prismjs": "^1.26.0", 64 | "svelte-awesome": "^2.4.2", 65 | "ua-parser-js": "^1.0.2" 66 | }, 67 | "homepage": "https://chainlist.github.io/svelte-forms/", 68 | "repository": { 69 | "type": "git", 70 | "url": "https://github.com/chainlist/svelte-forms.git" 71 | }, 72 | "author": { 73 | "name": "Kevin Guillouard" 74 | }, 75 | "license": "MIT" 76 | } 77 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | const cssnano = require('cssnano'); 4 | 5 | const mode = process.env.NODE_ENV; 6 | const dev = mode === 'development'; 7 | 8 | const config = { 9 | plugins: [ 10 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 11 | tailwindcss(), 12 | //But others, like autoprefixer, need to run after, 13 | autoprefixer(), 14 | !dev && 15 | cssnano({ 16 | preset: 'default' 17 | }) 18 | ] 19 | }; 20 | 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in PostCSS syntax */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @layer base { 7 | html { 8 | font-size: 62.5%; 9 | } 10 | 11 | html, 12 | body { 13 | @apply text-gray-700; 14 | } 15 | 16 | #svelte { 17 | /* display: grid; 18 | grid-template-columns: 25rem 1fr; */ 19 | height: 100%; 20 | } 21 | 22 | h1 { 23 | @apply w-full text-7xl mb-20 text-gray-700 tracking-widest font-normal; 24 | } 25 | 26 | h2 { 27 | @apply w-full border-t-2 border-gray-300 pt-5 text-5xl text-gray-700 tracking-widest font-normal; 28 | font-variant: small-caps; 29 | } 30 | 31 | h3 { 32 | @apply w-full pt-5 text-3xl text-gray-500 tracking-widest font-normal; 33 | } 34 | 35 | p code, 36 | li code { 37 | @apply py-1 px-2 text-red-500 shadow; 38 | } 39 | 40 | blockquote { 41 | @apply bg-red-100 p-5 rounded-md text-gray-700; 42 | } 43 | 44 | section ul { 45 | @apply pl-5; 46 | } 47 | 48 | section ul li:before { 49 | content: '\279C'; 50 | @apply text-red-400 mr-3; 51 | } 52 | 53 | p a { 54 | @apply text-red-700 hover:underline; 55 | } 56 | } 57 | 58 | @layer utilities { 59 | .border-1 { 60 | border-width: 1px; 61 | } 62 | 63 | .border-t-1 { 64 | border-top-width: 1px; 65 | } 66 | .border-b-1 { 67 | border-bottom-width: 1px; 68 | } 69 | 70 | .border-l-1 { 71 | border-left-width: 1px; 72 | } 73 | .border-r-1 { 74 | border-right-width: 1px; 75 | } 76 | } 77 | 78 | @layer components { 79 | .main-link { 80 | @apply text-gray-700 text-3xl; 81 | font-variant: small-caps; 82 | } 83 | 84 | .main-link:not(:first-child) { 85 | @apply mt-10; 86 | } 87 | 88 | .sub-link { 89 | @apply text-gray-500 text-2xl; 90 | @apply pl-5; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %svelte.head% 10 | 11 | 12 |
%svelte.body%
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/Debug.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
(open = !open)}> 21 | 26 | {$field.dirty ? '[Dirty]' : ''} 27 | {$field.name ?? 'form'}{$field.errors.length ? `(${$field.errors.length})` : ''} 28 | 29 | 30 | {#if open} 31 |
32 | {$field.errors?.join(', ')} 33 |
34 | {/if} 35 |
36 | 37 | 39 | -------------------------------------------------------------------------------- /src/docs/1_Install.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 1_Install.md 3 | --- 4 | 5 | [![https://app.travis-ci.com/chainlist/svelte-forms.svg?branch=master](https://app.travis-ci.com/chainlist/svelte-forms.svg?branch=master)](https://app.travis-ci.com/github/chainlist/svelte-forms) 6 | 7 | ## getting started 8 | 9 | ### why 10 | 11 | `svelte-forms` first came out because there were back then, no real form validation library for svelte. I then decided to create one that was easy to use... or I thought it was. 12 | 13 | The first version was taking advantage of the `onMount` method that could be used anywhere in the code and not just in a component; but a lot of issues started to arrive when [sveltekit](https://kit.svelte.dev/) was announced. 14 | Something shady, that was not really easy to fix because the library was tightly coupled to that famous `onMount` function. 15 | 16 | A version 2 was quickly needed. Easier to use and easier to work on because it is developed using the sveltekit `package` command. 17 | That makes possible to create the library as well as the documentation at the same time. 18 | 19 | - So today, here we are with the v2 of `svelte-forms` that is hopefully easier to use and less bug prone for everyone! 20 | 21 | ### install 22 | 23 | To install `svelte-forms` do one of the following commands: 24 | 25 | ```bash 26 | npm install svelte-forms 27 | ``` 28 | 29 | ```bash 30 | pnpm add svelte-forms 31 | ``` 32 | 33 | ```bash 34 | yarn add svelte-forms 35 | ``` 36 | 37 | ### svelte-kit / sapper 38 | 39 | `svelte-forms` V2 has been made with the help of `svelte-kit`. 40 | 41 | And it runs smoothly with `svelte-kit` or `sapper`, so enjoy! 42 | 43 | ### github 44 | 45 | Check-out the [github repository](https://github.com/chainlist/svelte-forms) if you want to contribute 46 | -------------------------------------------------------------------------------- /src/docs/2_Examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 2_Examples.md 3 | --- 4 | 5 | ## some examples 6 | 7 | ### Simple form with one input 8 | 9 | ```svelte 10 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | ``` 25 | 26 | ### Reset a form / field 27 | 28 | ```svelte 29 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | ``` 48 | -------------------------------------------------------------------------------- /src/docs/3_Form.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 3_Form.md 3 | --- 4 | 5 | ## form 6 | 7 | ```typescript 8 | function form(...fields: Writable>[]) => Readable<{ 9 | dirty: boolean; 10 | valid: boolean; 11 | errors: string[]; 12 | hasError: (s: string) => boolean 13 | }> 14 | ``` 15 | 16 | `form(...fields)` returns a store and is a convenient function to bind all your `field` all together and reflect their internal state. 17 | 18 | ### functions 19 | 20 | In addition there is two different functions that can be called on the store: 21 | 22 | - `reset()` which resets all the binded `field` 23 | - `validate()` which launches a manual validation of all the binded `field` 24 | - `getField(name: string): Writable>` which returns a previously binded `field`. Useful when you pass your form around components. 25 | - `summary()` returns an object that represents the current state of all the fields that have been linked to the form 26 | 27 | ### hasError 28 | 29 | > There is also the method `hasError(s: string)` that can be called. 30 | > It is a method because it is not binded to the store directly but on its content. 31 | > You will then need to use the `$` in front of your variable to access the method `hasError` 32 | 33 | ```svelte 34 | 38 | ``` 39 | 40 | ### example 41 | 42 | ```svelte 43 | 50 | 51 |
52 | 53 | 54 | {#if $myForm.hasError('name.required')} 55 |
Name is required
56 | {/if} 57 |
58 | ``` 59 | -------------------------------------------------------------------------------- /src/docs/4_Field.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 4_Field.md 3 | --- 4 | 5 | ## field 6 | 7 | ```typescript 8 | function field( 9 | name: string, 10 | value: T, 11 | validators: Validator[] = [], 12 | options: FieldOptions = defaultOptions 13 | ); 14 | ``` 15 | 16 | `field()` returns a writable store and is a convenient function to create a new form input that will serve as your input controller. 17 | 18 | You can directly use the store to set the field value programatically if you need to. 19 | 20 | ```typescript 21 | import { field } from 'svelte-forms'; 22 | import { get } from 'svelte/store'; 23 | 24 | const name = field('name', ''); 25 | 26 | // Prefered method to set value programatically 27 | name.set('New value'); 28 | // or 29 | $name = 'New value'; 30 | 31 | // You can also do that 32 | const fieldObj = get(name); 33 | fieldObject.value = 'New value'; 34 | 35 | name.set(fieldObj); 36 | // or 37 | $name = fieldObj; 38 | 39 | // All cases work 40 | ``` 41 | 42 | ### functions 43 | 44 | In addition there is two different functions that can be called on the store: 45 | 46 | - `reset()` which resets the field to its default value (value passed when created) 47 | - `clear()` which clear the field and set its value to `null` 48 | - `validate()` which launches a validation on itself 49 | 50 | ### options 51 | 52 | ```typescript 53 | type FieldOptions = { 54 | valid: boolean; // default: true. Determines if the field is valid or not by default 55 | checkOnInit: boolean; // default: false. Launches a validation when the input is first rendered 56 | validateOnChange: boolean; // default: true. Launches a validations every time the input changes 57 | stopAtFirstError: boolean; // default: false. Stop checking for others validators if one fails 58 | }; 59 | ``` 60 | 61 | > There is no global form options to setup. Only per field options 62 | 63 | ### example 64 | 65 | ```svelte 66 | 75 | 76 |
77 | 78 | 79 | {#if $myForm.hasError('name.required')} 80 |
Name is required
81 | {/if} 82 | 83 | 84 |
85 | ``` 86 | -------------------------------------------------------------------------------- /src/docs/5_Combined.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 5_Combined.md 3 | --- 4 | 5 | ## combined 6 | 7 | ```typescript 8 | export function combined( 9 | name: string, 10 | fields: Field[], 11 | reducer: (fields: Field[]) => T, 12 | validators: Validator[] = [], 13 | options: FieldOptions = defaultFieldOptions 14 | ): Readable>; 15 | ``` 16 | 17 | `combined()` returns a readable store that is somewhat considered as a `field` that combines multiple fields together to have a single output. 18 | 19 | - Any binded field will pass their errors prefixed with the field name. 20 | 21 | ```typescript 22 | const firstname = field('firstname', '', [required()]); 23 | const lastname = field('lastname', '', [required()]); 24 | const fullname = combined('fullname', [firstname, lastname], ([firstname, lastname]) => [firstname.value, lastname.value].join(' ')); 25 | 26 | const myForm(firstname, lastname, fullname); 27 | 28 | firstname.validate(); 29 | lastname.validate(); 30 | 31 | // fullname.errors will contain the following error ['firstname.required', 'lastname.required'] 32 | ``` 33 | 34 | - You can also pass some validators to that function as well. These validators will get the reduced value that comes from your custom function as the 3rd argument. 35 | 36 | ### functions 37 | 38 | > combined create an object that is strictly **reactive**. Thus we cannot interact with it through functions calls 39 | 40 | ### options 41 | 42 | ```typescript 43 | type CombinedOptions = { 44 | stopAtFirstError: boolean; // default: false. Stop checking for others validators if one fails 45 | }; 46 | // Only available for validators that are set for that field. Not for the binded fields. 47 | ``` 48 | 49 | ### example 50 | 51 | ```svelte 52 | 62 | 63 |
64 | 65 | 66 | 67 | {#if $myForm.hasError('firstname.required')} 68 |
Firstname is required
69 | {/if} 70 | 71 | {#if $myForm.hasError('lastname.required')} 72 |
Lastname is required
73 | {/if} 74 | 75 |
Welcome {$fullname.value}
76 | 77 | 78 |
79 | ``` 80 | -------------------------------------------------------------------------------- /src/docs/6_Validators.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 6_Validators.md 3 | --- 4 | 5 | ## Validators 6 | 7 | - validators now need to be called directly, thus providing type safe auto-completion 8 | - validators now return a function that return an object `{ valid: boolean, name: string = 'validator_name' }` 9 | 10 | Check [custom validators](#custom-validator) for more info 11 | 12 | ### required 13 | 14 | ```typescript 15 | function required() => { valid: boolean, name : 'required' }; 16 | ``` 17 | 18 | ```typescript 19 | import { field } from 'svelte-forms'; 20 | import { required } from 'svelte-forms/validators'; 21 | 22 | const name = field('name', '', [required()]); 23 | ``` 24 | 25 | ### email 26 | 27 | ```typescript 28 | function email() => { valid: boolean, name : 'not_an_email' }; 29 | ``` 30 | 31 | ```typescript 32 | import { field } from 'svelte-forms'; 33 | import { email } from 'svelte-forms/validators'; 34 | 35 | const name = field('name', '', [email()]); 36 | ``` 37 | 38 | ### url 39 | 40 | ```typescript 41 | function url() => { valid: boolean, name : 'url' }; 42 | ``` 43 | 44 | ```typescript 45 | import { field } from 'svelte-forms'; 46 | import { url } from 'svelte-forms/validators'; 47 | 48 | const name = field('name', '', [url()]); 49 | ``` 50 | 51 | ### min 52 | 53 | ```typescript 54 | function min(n: number) => { valid: boolean, name : 'min' }; 55 | ``` 56 | 57 | ```typescript 58 | import { field } from 'svelte-forms'; 59 | import { min } from 'svelte-forms/validators'; 60 | 61 | const name = field('name', '', [min(3)]); 62 | 63 | // also works with numerical value 64 | const age = field('age', 0, [min(18)]); 65 | ``` 66 | 67 | ### max 68 | 69 | ```typescript 70 | function max(n: number) => { valid: boolean, name : 'max' }; 71 | ``` 72 | 73 | ```typescript 74 | import { field } from 'svelte-forms'; 75 | import { max } from 'svelte-forms/validators'; 76 | 77 | const name = field('name', '', [max(10)]); 78 | 79 | // also works with numerical value 80 | const age = field('age', 0, [max(18)]); 81 | ``` 82 | 83 | ### between 84 | 85 | ```typescript 86 | function between(n: number) => { valid: boolean, name : 'between' }; 87 | ``` 88 | 89 | ```typescript 90 | import { field } from 'svelte-forms'; 91 | import { between } from 'svelte-forms/validators'; 92 | 93 | const name = field('name', '', [between(3, 10)]); 94 | 95 | // equivalent to 96 | const name = field('name', '', [min(3), max(10)]); 97 | 98 | // also works with numerical value 99 | const age = field('age', 0, [between(0, 18)]); 100 | ``` 101 | 102 | ### pattern 103 | 104 | ```typescript 105 | function pattern(reg: RegEx) => { valid: boolean, name : 'pattern' }; 106 | ``` 107 | 108 | ```typescript 109 | import { field } from 'svelte-forms'; 110 | import { pattern } from 'svelte-forms/validators'; 111 | 112 | const age = field('age', '', [pattern(/\d+/)]); 113 | const firstname = field('name', '', [pattern(/\w+/)]); 114 | ``` 115 | 116 | ### matchField 117 | 118 | ```typescript 119 | function matchField(store: Readable>) => { valid: boolean, name : 'match_field' }; 120 | ``` 121 | 122 | ```typescript 123 | import { form, field } from 'svelte-forms'; 124 | import { matchField } from 'svelte-forms/validators'; 125 | 126 | const password = field('password', ''); 127 | const passwordConfirmation = field('passwordConfirmation', '', [matchField(password)]); 128 | const myForm = form(password, passwordConfirmation); 129 | 130 | if ($myForm.hasError('passwordConfirmation.match_field')) { 131 | alert('password do not match'); 132 | } 133 | ``` 134 | 135 | ### not 136 | 137 | Special validator to inverse the result of the passed validator. 138 | 139 | - The returned name will be the name of the passed validator as well, see the next example 140 | 141 | ```typescript 142 | function not(validator: Validator) => { valid: boolean, name : validation.name }; 143 | ``` 144 | 145 | ```typescript 146 | import { form, field } from 'svelte-forms'; 147 | import { not, between } from 'svelte-forms/validators'; 148 | 149 | const age = field('age', 0, [not(between(0, 17))]); 150 | 151 | const myForm = form(age); 152 | 153 | if ($myForm.hasError('age.between')) { 154 | alert('you should not be between 0 and 18 to access this website'); 155 | } 156 | ``` 157 | 158 | ### custom validator 159 | 160 | A validator is just a function that returns a function `(value: any) => { valid: boolean, name: 'name_of_the_validator' }`. Nothing else. 161 | 162 | Of course this validator can be asynchronus and return a promise. 163 | 164 | > If your validators takes parameters, they need to be put on the main function and not the returned one 165 | 166 | ```svelte 167 | 184 | 185 | 186 | ``` 187 | -------------------------------------------------------------------------------- /src/docs/7_UseStyle.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 7_UseStyle.md 3 | --- 4 | 5 | ## use:style 6 | 7 | ```typescript 8 | function style({ field: store: Readable>, valid = "valid", invalid = "invalid", dirty = "dirty" }); 9 | ``` 10 | 11 | `use:style` will help you to **automaticaly** set the class of your HTML field to `valid` or `invalid` only if the field is `dirty`. 12 | 13 | You can of course override the class names by setting your own. 14 | 15 | You will need to use to `:global()` CSS keyword to customize to look of those classes. 16 | 17 | ```svelte 18 | 23 | 24 | 25 | 26 | 27 | 44 | ``` 45 | -------------------------------------------------------------------------------- /src/docs/8_Migrating.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 8_Migrating.md 3 | --- 4 | 5 | ## migrate from v1 6 | 7 | ### form initialization 8 | 9 | ```typescript 10 | /// v1 11 | import { form } from 'svelte-forms'; 12 | 13 | const myForm = form(() => { 14 | return { 15 | name: { value: '', validators: ['required', 'max:2'] } 16 | } 17 | })); 18 | 19 | // v2 20 | import { form, field } from 'svelte-forms'; 21 | import { required, max } from 'svelte-forms/validators'; 22 | 23 | const name = field('name', '', [required(), max(2)]); 24 | const myForm(name); 25 | ``` 26 | 27 | ### use:bindClass 28 | 29 | - `bindClass` is renamed `style` 30 | - use `field` instead of `form` as now the validation is made per field 31 | 32 | ```svelte 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | ### validators 45 | 46 | ```typescript 47 | // V1 48 | function max(value: any, args: any[]) { 49 | const maxValue = parseFloat(args[0]); 50 | const value = isNaN(val) ? val.length : parseFloat(val); 51 | 52 | return value <= maxValue; 53 | } 54 | 55 | // V2 56 | function max(n: number) { 57 | return (value: any) => { 58 | const val = isNaN(value) ? value.length : parseFloat(value); 59 | 60 | return { valid: !isNaN(value) || val <= n, name: 'max' }; 61 | }; 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /src/docs/9_Contribute.md: -------------------------------------------------------------------------------- 1 | --- 2 | filename: 9_Contribute.md 3 | --- 4 | 5 | ## How to contribute 6 | 7 | The `npm` packages sources are inside `/src/lib` folder. 8 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.md' { 3 | // "unknown" would be more detailed depends on how you structure frontmatter 4 | const attributes: Record; 5 | 6 | // When "Mode.TOC" is requested 7 | const toc: { level: string; content: string }[]; 8 | 9 | // When "Mode.HTML" is requested 10 | const html: string; 11 | 12 | // When "Mode.React" is requested. VFC could take a generic like React.VFC<{ MyComponent: TypeOfMyComponent }> 13 | import React from 'react'; 14 | const ReactComponent: React.VFC; 15 | 16 | // When "Mode.Vue" is requested 17 | import { ComponentOptions, Component } from 'vue'; 18 | const VueComponent: ComponentOptions; 19 | const VueComponentWith: (components: Record) => ComponentOptions; 20 | 21 | // Modify below per your usage 22 | export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith }; 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/combined.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validators/validator.js'; 2 | import type { Readable } from 'svelte/store'; 3 | import { get } from 'svelte/store'; 4 | import { derived } from 'svelte/store'; 5 | import type { Field, FieldOptions, FieldsValues } from './types.js'; 6 | import { createFieldOject, getErrors } from './createFieldStore.js'; 7 | import { defaultFieldOptions } from './types.js'; 8 | 9 | export function combined>[], T>( 10 | name: string, 11 | fields: S, 12 | reducer: (fields: FieldsValues) => T, 13 | validators: Validator[] = [], 14 | options: Pick = defaultFieldOptions 15 | ): Readable> & { validate: () => Promise> } { 16 | let resolve: Promise>; 17 | const { subscribe } = derived>( 18 | fields, 19 | (values, set) => { 20 | const value = reducer(values); 21 | 22 | const createValidations = () => { 23 | let errors = []; 24 | 25 | values.forEach((value) => { 26 | errors = [ 27 | ...errors, 28 | ...value.errors 29 | .map((e) => { 30 | return { valid: false, name: `${value.name}.${e}` }; 31 | }) 32 | .flat() 33 | ]; 34 | }); 35 | 36 | return errors; 37 | }; 38 | 39 | const validations = createValidations(); 40 | 41 | resolve = getErrors(value, validators, options.stopAtFirstError).then( 42 | (combinedValidations) => { 43 | const obj = createFieldOject(name, value, [...combinedValidations, ...validations], { 44 | dirty: values.some((v) => v.dirty) 45 | }); 46 | 47 | set(obj); 48 | return obj; 49 | } 50 | ); 51 | set( 52 | createFieldOject(name, value, validations, { 53 | dirty: values.some((v) => v.dirty) 54 | }) 55 | ); 56 | }, 57 | createFieldOject(name, reducer(fields.map((f) => get(f)) as any), []) 58 | ); 59 | 60 | return { 61 | subscribe, 62 | validate: async () => { 63 | return resolve; 64 | } 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/createFieldStore.ts: -------------------------------------------------------------------------------- 1 | import isPromise from 'is-promise'; 2 | import type { Writable, Updater, Readable } from 'svelte/store'; 3 | import { writable, get } from 'svelte/store'; 4 | import type { Field, FieldOptions } from './types.js'; 5 | import type { FieldValidation, Validator } from './validators/validator.js'; 6 | import { isField } from './types.js'; 7 | 8 | export function createFieldOject( 9 | name: string, 10 | value: T, 11 | errors: FieldValidation[] = [], 12 | partialField: Partial> = {} 13 | ): Field { 14 | const field: Field = { 15 | name, 16 | value, 17 | valid: true, 18 | invalid: false, 19 | errors: [], 20 | dirty: false 21 | }; 22 | 23 | return processField(field, errors, partialField); 24 | } 25 | 26 | export function getValue(value: T | Field | Readable>): T { 27 | const isStore = function (v: T | Field | Readable>): v is Readable> { 28 | return (value as Readable>).subscribe !== undefined; 29 | }; 30 | 31 | const isField = function (v: T | Field | Readable>): v is Field { 32 | return !!(value as Field).name && (value as Field).valid !== undefined; 33 | }; 34 | 35 | if (isStore(value)) { 36 | return get(value).value; 37 | } else if (isField(value)) { 38 | return value.value; 39 | } 40 | 41 | return value; 42 | } 43 | 44 | export async function getErrors( 45 | value: T | Field | Readable>, 46 | validators: Validator[], 47 | stopAtFirstError = false, 48 | isOptional = false, 49 | ) { 50 | const v = getValue(value); 51 | 52 | if (isOptional && !v) { 53 | return []; 54 | } 55 | 56 | let errors: FieldValidation[] = []; 57 | 58 | for (const validator of validators) { 59 | let result = validator(v); 60 | 61 | if (isPromise(result)) { 62 | result = await result; 63 | } 64 | 65 | if (stopAtFirstError && !result.valid) { 66 | errors = [result]; 67 | break; 68 | } 69 | 70 | errors = [...errors, result]; 71 | } 72 | 73 | return errors; 74 | } 75 | 76 | export function processField( 77 | field: Field, 78 | validations?: FieldValidation[], 79 | partialField: Partial> = {} 80 | ) { 81 | if (validations) { 82 | const errors = validations.filter((v) => !v.valid).map((v) => v.name); 83 | const valid = !errors.length; 84 | return { ...field, valid: valid, invalid: !valid, errors, ...partialField }; 85 | // return { ...field, dirty: field.dirty || !!validations.length, valid, invalid: !valid, errors, ...partialField }; 86 | } 87 | 88 | return field; 89 | } 90 | 91 | export function createFieldStore( 92 | name: string, 93 | v: T, 94 | validators: Validator[] = [], 95 | options: FieldOptions 96 | ): Omit>, 'set'> & { 97 | validate: () => Promise>; 98 | reset: () => void; 99 | clear: () => void; 100 | set(this: void, value: Field | T): void; 101 | } { 102 | const value = { 103 | name, 104 | value: v, 105 | valid: options.valid, 106 | invalid: !options.valid, 107 | dirty: false, 108 | errors: [] 109 | }; 110 | const store = writable>(value); 111 | const { subscribe, update, set: _set } = store; 112 | 113 | async function set(this: void, field: Field | T, forceValidation: boolean = false) { 114 | if (!isField(field)) { 115 | field = processField(get(store), [], { value: field }); 116 | } 117 | 118 | if (forceValidation || options.validateOnChange) { 119 | let validations = await getErrors(field, validators, options.stopAtFirstError, options.isOptional); 120 | _set(processField(field, validations, { dirty: true })); 121 | } else { 122 | _set(processField(field, null, { dirty: true })); 123 | } 124 | } 125 | 126 | async function validate() { 127 | const errors = await getErrors(store, validators, options.stopAtFirstError, options.isOptional); 128 | let obj: Field; 129 | 130 | update((field) => { 131 | obj = processField(field, errors, { dirty: false }); 132 | return obj; 133 | }); 134 | 135 | return obj; 136 | } 137 | 138 | function reset() { 139 | _set( 140 | processField({ 141 | dirty: false, 142 | errors: [], 143 | name, 144 | valid: options.valid, 145 | invalid: !options.valid, 146 | value: v 147 | }) 148 | ); 149 | } 150 | 151 | if (options.checkOnInit) { 152 | set(value); 153 | } 154 | 155 | function clear() { 156 | _set( 157 | processField({ 158 | dirty: false, 159 | errors: [], 160 | name, 161 | valid: options.valid, 162 | invalid: !options.valid, 163 | value: null 164 | }) 165 | ); 166 | } 167 | 168 | return { subscribe, update, set, validate, reset, clear }; 169 | } 170 | -------------------------------------------------------------------------------- /src/lib/field.ts: -------------------------------------------------------------------------------- 1 | import { createFieldStore } from './createFieldStore.js'; 2 | import type { FieldOptions } from './types.js'; 3 | import { defaultFieldOptions } from './types.js'; 4 | import type { Validator } from './validators/validator.js'; 5 | 6 | export function field( 7 | name: string, 8 | value: T, 9 | validators: Validator[] = [], 10 | options: Partial = {} 11 | ) { 12 | return createFieldStore(name, value, validators, { ...defaultFieldOptions, ...options }); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/form.ts: -------------------------------------------------------------------------------- 1 | import type { Readable, Writable } from 'svelte/store'; 2 | import { derived, get } from 'svelte/store'; 3 | import type { Field } from './types.js'; 4 | 5 | export type Form = { 6 | valid: boolean; 7 | dirty: boolean; 8 | errors: string[]; 9 | }; 10 | 11 | export function form(...fields: (Writable> | Readable>)[]) { 12 | let names: string[] = []; 13 | let doubles: string[] = []; 14 | 15 | fields.forEach((field) => { 16 | const obj = get(field); 17 | if (names.includes(obj.name)) { 18 | doubles = doubles.includes(obj.name) ? doubles : [...doubles, obj.name]; 19 | } else { 20 | names = [...names, obj.name]; 21 | } 22 | }); 23 | 24 | if (doubles.length) { 25 | throw new Error(`Cannot have the fields with the same name: ${doubles.join(', ')}`); 26 | } 27 | 28 | const store = derived(fields, (values) => { 29 | return { 30 | valid: values.every((value) => value.valid), 31 | dirty: values.some((value) => value.dirty), 32 | 33 | // Summary as a getter to avoid useless computation of data 34 | // if no one wants it 35 | get summary() { 36 | return values.reduce((carry, f) => { 37 | carry[f.name] = f.value; 38 | 39 | return carry; 40 | }, {}); 41 | }, 42 | 43 | errors: values 44 | .map((value) => { 45 | return value.errors.map((e) => { 46 | if (e.includes('.')) { 47 | return e; 48 | } 49 | 50 | return `${value.name}.${e}`; 51 | }); 52 | }) 53 | .flat() 54 | .filter((value, index, self) => self.indexOf(value) === index), 55 | 56 | hasError(this: Form, name: string) { 57 | return this.errors.findIndex((e) => e === name) !== -1; 58 | } 59 | }; 60 | }); 61 | 62 | const { subscribe } = store; 63 | 64 | function reset() { 65 | fields.forEach((field: any) => field.reset && field.reset()); 66 | } 67 | 68 | function clear() { 69 | fields.forEach((field: any) => field.clear && field.clear()); 70 | } 71 | 72 | async function validate() { 73 | for (const field of fields) { 74 | if ((field as any).validate) await (field as any).validate(); 75 | } 76 | } 77 | 78 | function getField(name: string): Writable> | Readable> { 79 | return fields.find((f) => get(f).name === name); 80 | } 81 | 82 | function summary(): Record { 83 | return get(store).summary; 84 | } 85 | 86 | return { subscribe, reset, validate, getField, summary, clear }; 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export type { Validator, FieldValidation } from './validators/validator.js'; 2 | export { form } from './form.js'; 3 | export { field } from './field.js'; 4 | export { style } from './use.style.js'; 5 | export { combined } from './combined.js'; 6 | export { defaultFieldOptions as defaultFieldOptions } from './types.js'; 7 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Readable } from 'svelte/store'; 2 | 3 | export type FieldOptions = { 4 | valid: boolean; 5 | checkOnInit: boolean; 6 | validateOnChange: boolean; 7 | stopAtFirstError: boolean; 8 | isOptional: boolean; 9 | }; 10 | 11 | export type Field = { 12 | name: string; 13 | value: T; 14 | valid: boolean; 15 | invalid: boolean; 16 | dirty: boolean; 17 | errors: string[]; 18 | }; 19 | 20 | export const defaultFieldOptions: FieldOptions = { 21 | valid: true, 22 | checkOnInit: false, 23 | validateOnChange: true, 24 | stopAtFirstError: false, 25 | isOptional: false 26 | }; 27 | 28 | export type FieldsValues = T extends Readable 29 | ? U 30 | : { 31 | [K in keyof T]: T[K] extends Readable ? U : never; 32 | }; 33 | 34 | export type Fields = 35 | | Readable 36 | | [Readable, ...Array>] 37 | | Array>; 38 | 39 | export type Form = { 40 | valid: boolean; 41 | dirty: boolean; 42 | errors: string[]; 43 | }; 44 | 45 | export function isField(field: any): field is Field { 46 | const keys = Object.keys(field); 47 | return ['name', 'value', 'valid', 'invalid', 'errors'].every((key) => keys.includes(key)); 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/use.style.ts: -------------------------------------------------------------------------------- 1 | export function style( 2 | node: HTMLElement, 3 | { field = null, valid = 'valid', invalid = 'invalid', dirty = 'dirty' } = {} 4 | ) { 5 | const unsubscribe = field.subscribe((field) => { 6 | if (field.dirty) { 7 | node.classList.add(dirty); 8 | if (field.valid) { 9 | node.classList.add(valid); 10 | node.classList.remove(invalid); 11 | } else { 12 | node.classList.add(invalid); 13 | node.classList.remove(valid); 14 | } 15 | } else { 16 | node.classList.remove(dirty); 17 | } 18 | }); 19 | 20 | return { 21 | destroy: () => unsubscribe() 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/validators/between.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function between(min: number, max: number): Validator { 4 | return (value: any) => { 5 | const val = isNaN(value) ? value.length : parseFloat(value); 6 | return { valid: val >= min && val <= max, name: 'between' }; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/validators/email.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function email(): Validator { 4 | return (value: any) => { 5 | const regex = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/; 6 | return { valid: Boolean(value) && regex.test(value), name: 'not_an_email' }; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './between.js'; 2 | export * from './email.js'; 3 | export * from './max.js'; 4 | export * from './min.js'; 5 | export * from './required.js'; 6 | export * from './url.js'; 7 | export * from './matchField.js'; 8 | export * from './not.js'; 9 | export * from './pattern.js'; 10 | -------------------------------------------------------------------------------- /src/lib/validators/matchField.ts: -------------------------------------------------------------------------------- 1 | import type { Field } from '$lib/types'; 2 | import type { Readable } from 'svelte/store'; 3 | import { get } from 'svelte/store'; 4 | 5 | export function matchField(store: Readable>) { 6 | return (value) => { 7 | return { valid: get(store).value === value, name: 'match_field' }; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/validators/max.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function max(n: number): Validator { 4 | return (value: any) => { 5 | const val = typeof value === 'string' ? value.length : isNaN(value) ? 0 : parseFloat(value); 6 | 7 | return { valid: val <= n, name: 'max' }; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/validators/min.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function min(n: number): Validator { 4 | return (value: any) => { 5 | const val = isNaN(value) ? value.length : parseFloat(value); 6 | return { valid: val >= n, name: 'min' }; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/validators/not.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from '$lib'; 2 | import isPromise from 'is-promise'; 3 | 4 | export function not(validation: Validator) { 5 | return async (value: any) => { 6 | const validator = validation(value); 7 | 8 | if (isPromise(validator)) { 9 | const result = await validator; 10 | return { valid: !result.valid, name: result.name }; 11 | } 12 | 13 | return { valid: !validator.valid, name: validator.name }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/validators/pattern.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function pattern(pattern: RegExp): Validator { 4 | return (value: any) => { 5 | if (value === null || value === undefined) { 6 | return { valid: false, name: 'pattern' }; 7 | } 8 | 9 | return { valid: pattern.test(value), name: 'pattern' }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/validators/required.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function required(): Validator { 4 | return (val: string) => { 5 | let valid = true; 6 | if (val === undefined || val === null) valid = false; 7 | 8 | if (typeof val === 'string') { 9 | const tmp = val.replace(/\s/g, ''); 10 | 11 | valid = tmp.length > 0; 12 | } 13 | 14 | return { valid, name: 'required' }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/validators/url.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from './validator.js'; 2 | 3 | export function url(): Validator { 4 | const regex = 5 | /(https?|ftp|git|svn):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/i; 6 | return (value: string) => ({ valid: regex.test(value), name: 'url' }); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/validators/validator.ts: -------------------------------------------------------------------------------- 1 | export type FieldValidation = { valid: boolean; name: string }; 2 | export type Validator = (value: any) => FieldValidation | Promise; 3 | -------------------------------------------------------------------------------- /src/routes/__layout.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | svelte-forms: Forms validation made easy 37 | 38 | 39 | 53 | 54 | {#if open} 55 | 75 | {/if} 76 | 77 |
78 | 79 |
80 | -------------------------------------------------------------------------------- /src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 |

svelte-forms documentation

29 | {#each docs as asyncDoc} 30 | {#await asyncDoc then doc} 31 |
32 | 37 | edit documentation 38 | 39 | {@html doc.html} 40 |
41 | {/await} 42 | {/each} 43 |
44 | -------------------------------------------------------------------------------- /src/routes/test/__layout.reset.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/routes/test/index.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 	{JSON.stringify($name, null, 2)}
19 | 
20 | 21 |
22 | 	{JSON.stringify($myForm, null, 2)}
23 | 
24 | 25 | 31 | -------------------------------------------------------------------------------- /src/tests/combined.test.ts: -------------------------------------------------------------------------------- 1 | import { combined, field } from '$lib'; 2 | import { max, min } from '$lib/validators'; 3 | import { get } from 'svelte/store'; 4 | 5 | const flushPromises = () => new Promise((resolve) => setTimeout(resolve)); 6 | 7 | describe('combined', () => { 8 | it('should construct a combined object', () => { 9 | const a = field('a', 1); 10 | const b = field('b', 2); 11 | 12 | const c = combined('fullname', [a, b], ([c, d]) => c.value + d.value); 13 | 14 | const obj = get(c); 15 | expect(obj.value).toBe(3); 16 | expect(obj.valid).toBeTruthy(); 17 | expect(obj.invalid).toBeFalsy(); 18 | expect(obj.dirty).toBeFalsy(); 19 | }); 20 | 21 | it('should be raise parents error', async () => { 22 | jest.useFakeTimers(); 23 | const a = field('a', 1, [min(2)]); 24 | const b = field('b', 2); 25 | 26 | const c = combined('fullname', [a, b], ([c, d]) => c.value + d.value); 27 | 28 | await a.validate(); 29 | const values = []; 30 | 31 | c.subscribe((data) => { 32 | values.push(data); 33 | }); 34 | 35 | expect(get(c)).toEqual({ 36 | dirty: false, 37 | errors: ['a.min'], 38 | invalid: true, 39 | name: 'fullname', 40 | valid: false, 41 | value: 3 42 | }); 43 | }); 44 | 45 | it('should be raise parents error', async () => { 46 | jest.useFakeTimers(); 47 | const a = field('a', 1, [min(2)]); 48 | const b = field('b', 4, [max(2)]); 49 | 50 | const c = combined('fullname', [a, b], ([c, d]) => c.value + d.value); 51 | 52 | await a.validate(); 53 | await b.validate(); 54 | const values = []; 55 | 56 | c.subscribe((data) => { 57 | values.push(data); 58 | }); 59 | 60 | const obj = get(c); 61 | expect(obj.errors).toEqual(['a.min', 'b.max']); 62 | }); 63 | 64 | it('should run its own validator', async () => { 65 | jest.useFakeTimers(); 66 | const a = field('a', 1); 67 | const b = field('b', 4); 68 | 69 | const newMax = (n: number) => { 70 | return (value: number) => { 71 | return { valid: value <= n, name: 'newMax' }; 72 | }; 73 | }; 74 | 75 | const c = combined('fullname', [a, b], ([c, d]) => c.value + d.value, [newMax(2)]); 76 | 77 | await a.validate(); 78 | await b.validate(); 79 | 80 | const values = []; 81 | 82 | c.subscribe((data) => { 83 | values.push(data); 84 | }); 85 | 86 | const obj = await c.validate(); 87 | 88 | expect(obj.errors).toEqual(['newMax']); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/tests/field.test.ts: -------------------------------------------------------------------------------- 1 | import { field } from '$lib'; 2 | import { min, required, email } from '$lib/validators'; 3 | import { get } from 'svelte/store'; 4 | 5 | describe('field()', () => { 6 | it('should handle with no validator', () => { 7 | const name = field('name', ''); 8 | name.validate(); 9 | 10 | const result = get(name); 11 | 12 | expect(result.valid).toBe(true); 13 | }); 14 | 15 | describe('with one validator', () => { 16 | it('should be valid', () => { 17 | const name = field('name', 'not empty', [required()]); 18 | name.validate(); 19 | 20 | const result = get(name); 21 | 22 | expect(result.valid).toBe(true); 23 | expect(result.errors).not.toContain('required'); 24 | }); 25 | 26 | it('should not be valid', async () => { 27 | const name = field('name', '', [required()]); 28 | await name.validate(); 29 | 30 | const result = get(name); 31 | 32 | expect(result.valid).toBe(false); 33 | expect(result.errors).toContain('required'); 34 | }); 35 | }); 36 | 37 | describe('with multiples validators', () => { 38 | it('should be valid', () => { 39 | const name = field('name', 'not empty', [required(), min(3)]); 40 | name.validate(); 41 | 42 | const result = get(name); 43 | 44 | expect(result.valid).toBe(true); 45 | expect(result.errors).not.toContain('required'); 46 | expect(result.errors).not.toContain('min'); 47 | }); 48 | 49 | it('should not be valid with one failed validator', async () => { 50 | const name = field('name', 'no', [required(), min(3)]); 51 | await name.validate(); 52 | 53 | const result = get(name); 54 | 55 | expect(result.valid).toBe(false); 56 | expect(result.errors).not.toContain('required'); 57 | expect(result.errors).toContain('min'); 58 | }); 59 | 60 | it('should not be valid with two failed validators', async () => { 61 | const name = field('name', '', [required(), min(3)]); 62 | await name.validate(); 63 | 64 | const result = get(name); 65 | 66 | expect(result.valid).toBe(false); 67 | expect(result.errors).toContain('required'); 68 | expect(result.errors).toContain('min'); 69 | }); 70 | 71 | it('we should be able to set with the value directly', async () => { 72 | const name = field('name', ''); 73 | 74 | name.set('new value'); 75 | await name.validate(); 76 | 77 | const result = get(name); 78 | 79 | expect(result.value).toEqual('new value'); 80 | }); 81 | 82 | it('we should be able to set with a field value', async () => { 83 | const name = field('name', ''); 84 | 85 | let f = get(name); 86 | 87 | f.value = 'new field value'; 88 | name.set(f); 89 | await name.validate(); 90 | 91 | const result = get(name); 92 | 93 | expect(result.value).toEqual('new field value'); 94 | }); 95 | 96 | it('we should be able to reset a field', async () => { 97 | const name = field('name', 'default value'); 98 | 99 | let f = get(name); 100 | f.value = 'new value'; 101 | name.reset(); 102 | 103 | const result = get(name); 104 | expect(result.value).toEqual('default value'); 105 | }); 106 | 107 | it('we should be able to clear a field', async () => { 108 | const name = field('name', 'default value'); 109 | 110 | let f = get(name); 111 | f.value = 'new value'; 112 | name.clear(); 113 | 114 | const result = get(name); 115 | expect(result.value).toBeNull(); 116 | }); 117 | }); 118 | 119 | it('should remain invalid on change', async () => { 120 | let emailOptions = { validateOnChange: false, valid: false }; 121 | const emailField = field('emailField', "", [email()], emailOptions); 122 | 123 | emailField.set(get(field('emailField', 'not an email', [email()], emailOptions))); 124 | expect(get(emailField).valid).toEqual(false); 125 | 126 | emailField.set(get(field('emailField', 'hello@email.com', [email()], emailOptions))); 127 | expect(get(emailField).valid).toEqual(false); 128 | 129 | await emailField.validate(); 130 | expect(get(emailField).valid).toEqual(true); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/tests/form.test.ts: -------------------------------------------------------------------------------- 1 | import { field, form } from '$lib'; 2 | import { required } from '$lib/validators'; 3 | import { get } from 'svelte/store'; 4 | 5 | describe('form()', () => { 6 | it('should create a form', () => { 7 | const name = field('name', ''); 8 | const myForm = form(name); 9 | 10 | myForm.validate(); 11 | 12 | const result = get(myForm); 13 | 14 | expect(result.valid).toBe(true); 15 | }); 16 | 17 | it('should not be valid', async () => { 18 | const name = field('name', '', [required()]); 19 | const myForm = form(name); 20 | 21 | await myForm.validate(); 22 | 23 | const result = get(myForm); 24 | 25 | expect(result.valid).toBe(false); 26 | expect(result.hasError('name.required')).toBe(true); 27 | }); 28 | 29 | it('should not be possible to add multiples field with the same name', () => { 30 | const name = field('name', '', [required()]); 31 | 32 | expect(() => form(name, name)).toThrowError(); 33 | }); 34 | 35 | it('should create a form summary', () => { 36 | const firstName = field('firstName', 'John'); 37 | const lastName = field('lastName', 'Doe'); 38 | const myForm = form(firstName, lastName); 39 | 40 | expect(myForm.summary()).toEqual({ 41 | firstName: 'John', 42 | lastName: 'Doe' 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/tests/validators/between.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { between } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'between(min, max)', 7 | (opts) => { 8 | const result = between(opts.min, opts.max)(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('between'); 12 | }, 13 | { 14 | 'with numberical value in range': { min: 0, max: 10, value: 5, expected: true }, 15 | 'with numberical value above range': { min: 0, max: 10, value: 11, expected: false }, 16 | 'with numberical value below range': { min: 0, max: 10, value: -1, expected: false }, 17 | 'with numberical value at lower limit': { min: 0, max: 10, value: 0, expected: true }, 18 | 'with numberical value at upper limit': { min: 0, max: 10, value: 10, expected: true }, 19 | 20 | 'with string value in range': { min: 0, max: 10, value: 'in range', expected: true }, 21 | 'with string value above range': { min: 0, max: 10, value: 'not in range', expected: false }, 22 | 'with string value below range': { min: 0, max: 10, value: '', expected: false }, 23 | 'with string value at upper limit': { min: 0, max: 10, value: 'test test ', expected: true }, 24 | 'with string value at lower limit': { min: 0, max: 10, value: 'tes', expected: true } 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /src/tests/validators/email.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { email } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'email(value)', 7 | (opts) => { 8 | const result = email()(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('not_an_email'); 12 | }, 13 | { 14 | 'with numerical': { value: 12921, expected: false }, 15 | 16 | 'with good email': { value: 'example@example.com', expected: true }, 17 | 'with bad email': { value: 'example@example', expected: false }, 18 | 'with only blank spaces': { value: ' ', expected: false }, 19 | 'with empty string': { value: '', expected: false }, 20 | 'with empty undefined': { value: undefined, expected: false }, 21 | 'with empty null': { value: null, expected: false } 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /src/tests/validators/matchField.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { matchField } from '$lib/validators'; 3 | import { field } from '$lib/field'; 4 | import type { FieldValidation } from '$lib'; 5 | 6 | cases( 7 | 'matchField(fieldStore)', 8 | (opts) => { 9 | const password = field('password', opts.value1); 10 | 11 | const result = matchField(password)(opts.value2) as FieldValidation; 12 | 13 | expect(result.valid).toBe(opts.expected); 14 | expect(result.name).toBe('match_field'); 15 | }, 16 | { 17 | 'with fields numerical matching': { value1: 1, value2: 1, expected: true }, 18 | 'with fields numerical not matching with string': { value1: 2, value2: '2', expected: false }, 19 | 'with fields numerical not matching': { value1: 1, value2: 2, expected: false }, 20 | 'with fields numerical not matching empty': { value1: 1, value2: undefined, expected: false }, 21 | 22 | 'with fields string matching': { value1: 'foo', value2: 'foo', expected: true }, 23 | 'with fields string not matching': { value1: 'foo', value2: 'bar', expected: false }, 24 | 'with fields string not matching with numerical': { value1: '2', value2: 2, expected: false }, 25 | 'with fields string not matching empty': { value1: 'foo', value2: '', expected: false } 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /src/tests/validators/max.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { max } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'max(max)', 7 | (opts) => { 8 | const result = max(opts.max)(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('max'); 12 | }, 13 | { 14 | 'with numberical value above max': { max: 3, value: 5, expected: false }, 15 | 'with numberical value below max': { max: 3, value: 1, expected: true }, 16 | 'with numberical value at max': { max: 3, value: 3, expected: true }, 17 | 18 | 'with string value above max': { max: 3, value: 'above max', expected: false }, 19 | 'with string value below max': { max: 3, value: 'n', expected: true }, 20 | 'with string value at max': { max: 3, value: 'tes', expected: true }, 21 | 'with string value empty': { max: 3, value: '', expected: true }, 22 | 23 | 'with undefined': { max: 3, value: undefined, expected: true } 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/tests/validators/min.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { min } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'min(min)', 7 | (opts) => { 8 | const result = min(opts.min)(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('min'); 12 | }, 13 | { 14 | 'with numberical value above min': { min: 3, value: 5, expected: true }, 15 | 'with numberical value below min': { min: 3, value: -1, expected: false }, 16 | 'with numberical value at min': { min: 3, value: 3, expected: true }, 17 | 18 | 'with string value above min': { min: 3, value: 'above min', expected: true }, 19 | 'with string value below min': { min: 3, value: 'n', expected: false }, 20 | 'with string value at min': { min: 3, value: 'tes', expected: true } 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /src/tests/validators/not.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { not, min, max, between, email, url, required } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'matchField(fieldStore)', 7 | async (opts) => { 8 | const result = (await not(opts.validator)(opts.value)) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | }, 12 | { 13 | 'with min above': { value: 1, validator: min(2), expected: true }, 14 | 'with min below': { value: 1, validator: min(0), expected: false }, 15 | 16 | 'with max above': { value: 1, validator: max(2), expected: false }, 17 | 'with max below': { value: 1, validator: max(0), expected: true }, 18 | 19 | 'with out range': { value: 11, validator: between(0, 10), expected: true }, 20 | 'with in range': { value: 5, validator: between(0, 10), expected: false }, 21 | 22 | 'is not an email': { value: 'example@s', validator: email(), expected: true }, 23 | 'is an email': { value: 'example@example.com', validator: email(), expected: false }, 24 | 25 | 'is not an url': { value: 'https://example', validator: url(), expected: true }, 26 | 'is an url': { value: 'https://example.com', validator: url(), expected: false }, 27 | 28 | 'is not required': { value: '', validator: required(), expected: true }, 29 | 'is required': { value: 'test', validator: required(), expected: false }, 30 | 31 | 'not not required': { value: 'test', validator: not(required()), expected: true } 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /src/tests/validators/pattern.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { pattern } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'pattern(regex)', 7 | (opts) => { 8 | const result = pattern(opts.pattern)(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('pattern'); 12 | }, 13 | { 14 | 'with numerical': { value: 12921, pattern: /\d+/, expected: true }, 15 | 'with numerical single digit': { value: 1, pattern: /\d/, expected: true }, 16 | 17 | 'with string wrong pattern': { value: 'hello', pattern: /\d+/, expected: false }, 18 | 'with string': { value: 'hello', pattern: /\w+/, expected: true }, 19 | 20 | 'with null value': { value: null, pattern: /\d+/, expected: false }, 21 | 'with undefined value': { value: undefined, pattern: /\w+/, expected: false } 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /src/tests/validators/required.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { required } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'required()', 7 | (opts) => { 8 | const result = required()(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('required'); 12 | }, 13 | { 14 | 'with numerical 0': { value: 0, expected: true }, 15 | 'with numerical': { value: 10, expected: true }, 16 | 17 | 'with non empty string': { value: 'non empty', expected: true }, 18 | 'with only blank spaces': { value: ' ', expected: false }, 19 | 'with empty string': { value: '', expected: false }, 20 | 'with empty undefined': { value: undefined, expected: false }, 21 | 'with empty null': { value: null, expected: false } 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /src/tests/validators/url.test.ts: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { url } from '$lib/validators'; 3 | import type { FieldValidation } from '$lib'; 4 | 5 | cases( 6 | 'url(value)', 7 | (opts) => { 8 | const result = url()(opts.value) as FieldValidation; 9 | 10 | expect(result.valid).toBe(opts.expected); 11 | expect(result.name).toBe('url'); 12 | }, 13 | { 14 | 'with numerical': { value: 12921, expected: false }, 15 | 16 | 'with good url https': { value: 'https://example.com', expected: true }, 17 | 'with good url https with subdomain': { value: 'https://www.example.com', expected: true }, 18 | 'with good url https with subdomain 2': { 19 | value: 'https://subdomain.example.com', 20 | expected: true 21 | }, 22 | 'with good url http': { value: 'http://example.com', expected: true }, 23 | 24 | 'with url without protocol': { value: 'example.com', expected: false }, 25 | 'with bad url': { value: 'https://ex', expected: false }, 26 | 'with only blank spaces': { value: ' ', expected: false }, 27 | 'with empty string': { value: '', expected: false }, 28 | 'with empty undefined': { value: undefined, expected: false }, 29 | 'with empty null': { value: null, expected: false } 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /src/utils/isPromise.ts: -------------------------------------------------------------------------------- 1 | export function isPormise(obj: PromiseLike | S): obj is PromiseLike { 2 | return ( 3 | !!obj && 4 | (typeof obj === 'object' || typeof obj === 'function') && 5 | typeof (obj as any).then === 'function' 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainlist/svelte-forms/50cacb9fb83193a73c07870a3c4d64734bb63621/static/favicon.png -------------------------------------------------------------------------------- /static/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.25.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+jsx+tsx+typescript */ 3 | code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import path from 'path'; 3 | import mdPlugin from 'vite-plugin-markdown'; 4 | import markdownIt from 'markdown-it'; 5 | import anchor from 'markdown-it-anchor'; 6 | import kebab from 'lodash.kebabcase'; 7 | import adapter from '@sveltejs/adapter-static'; 8 | 9 | const slugify = (s) => kebab(s); 10 | 11 | const mdit = markdownIt(); 12 | mdit.use(anchor, { slugify }); 13 | 14 | const isProduction = process.env.NODE_ENV === 'production'; 15 | 16 | /** @type {import('@sveltejs/kit').Config} */ 17 | const config = { 18 | // Consult https://github.com/sveltejs/svelte-preprocess 19 | // for more information about preprocessors 20 | preprocess: [ 21 | preprocess({ 22 | postcss: true 23 | }) 24 | ], 25 | 26 | kit: { 27 | adapter: adapter(), 28 | 29 | paths: { 30 | base: isProduction ? '/svelte-forms' : '', 31 | assets: isProduction ? 'https://chainlist.github.io/svelte-forms' : '' 32 | }, 33 | 34 | // hydrate the
element in src/app.html 35 | package: { 36 | exports: (file) => file.includes('index.ts') 37 | }, 38 | vite: { 39 | plugins: [mdPlugin.plugin({ mode: ['html', 'toc'], markdownIt: mdit })], 40 | resolve: { 41 | alias: { 42 | $components: path.resolve('src/components'), 43 | $utils: path.resolve('src/utils'), 44 | 'svelte-forms': path.resolve('src/lib'), 45 | 'svelte-forms/validators': path.resolve('src/lib/validators') 46 | } 47 | } 48 | } 49 | } 50 | }; 51 | 52 | export default config; 53 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | mode: 'jit', 3 | purge: ['./src/**/*.{html,js,svelte,ts}'], 4 | 5 | theme: { 6 | extend: {} 7 | }, 8 | 9 | plugins: [] 10 | }; 11 | 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2020", 5 | "lib": ["es2020", "DOM"], 6 | "target": "es2020", 7 | /** 8 | svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 9 | to enforce using \`import type\` instead of \`import\` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | To have warnings/errors of the Svelte compiler at the correct position, 16 | enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | "allowJs": true, 24 | "checkJs": true, 25 | "paths": { 26 | "$lib": ["src/lib"], 27 | "$lib/*": ["src/lib/*"], 28 | "$components": ["src/components"], 29 | "$components/*": ["src/components/*"], 30 | "$utils": ["src/utils"], 31 | "$utils/*": ["src/utils/*"], 32 | "svelte-forms": ["src/lib"], 33 | "svelte-forms/*": ["src/lib/*"], 34 | "svelte-forms/validators": ["src/lib/validators"], 35 | "svelte-forms/validators/*": ["src/lib/validators/*"] 36 | } 37 | }, 38 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] 39 | } 40 | --------------------------------------------------------------------------------