├── .github └── workflows │ └── publish-latest.yaml ├── .gitignore ├── .node-version ├── LICENSE ├── README.md ├── __tests__ ├── field.spec.ts ├── formsField.spec.ts ├── isValidForm.spec.ts ├── testReactivity.ts ├── toObject.spec-d.ts └── toObject.spec.ts ├── config └── vitest.ts ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.ts │ │ └── styles │ │ └── vars.css ├── api │ ├── defineForm.md │ ├── field.md │ ├── formsField.md │ ├── isValidForm.md │ ├── privateField.md │ └── toObject.md ├── components │ └── StackBlitz.vue ├── examples │ ├── array-of-forms-with-vue-draggable.md │ ├── array-of-forms.md │ ├── checkboxes-and-radio.md │ ├── cross-field-validation.md │ ├── dividing-form-components.md │ ├── multi-step-form-wizard.md │ ├── pseudo-async-validation.md │ └── simple-example.md ├── guide │ └── getting-started.md ├── index.md ├── public │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── logo.svg │ └── og.png └── types │ ├── FormPropType.md │ └── YupValidationError.md ├── examples ├── array-of-forms-with-vue-draggable │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── array-of-forms │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── cross-field-validation │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── dividing-form-components │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ └── UserForm.vue │ │ ├── forms.ts │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── multi-step-form-wizard │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ └── ErrorMessage.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── pseudo-async-validation │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── form.ts │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── simple-example │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── validating-checkbox-inputs │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── validating-radio-inputs │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ ├── main.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package-lock.json ├── package.json ├── src ├── index.ts ├── types.ts └── yup.ts └── tsconfig.json /.github/workflows/publish-latest.yaml: -------------------------------------------------------------------------------- 1 | name: Publish (latest) 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | id-token: write 9 | 10 | jobs: 11 | publish: 12 | if: "!github.event.release.prerelease" 13 | runs-on: ubuntu-latest 14 | environment: npm 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version-file: ".node-version" 21 | registry-url: "https://registry.npmjs.org" 22 | cache: "npm" 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm pkg delete scripts devDependencies 26 | - run: npm publish --provenance --tag=latest 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | coverage/ 5 | docs/.vitepress/dist/ 6 | docs/.vitepress/cache/ 7 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.17.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mascii 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 | # vue-yup-form 2 |

3 | 4 | vue-yup-form - Headless form validation with Vue and Yup 5 | 6 |
7 | Headless form validation with Vue and Yup 8 |

9 | 10 | ## Requirements 11 | The following versions are supported: 12 | 13 | - [Vue](https://www.npmjs.com/package/vue) 14 | - 3.x 15 | - 2.7.x 16 | - 2.6.14 + [`@vue/composition-api`](https://www.npmjs.com/package/@vue/composition-api) plugin 17 | - [yup](https://www.npmjs.com/package/yup) 18 | - `^1.0.0` 19 | - `^0.32.11` 20 | - [TypeScript](https://www.npmjs.com/package/typescript) 21 | - `>=4.4.0` 22 | 23 | ## Documentation 24 | To check out docs, visit [vue-yup-form.pages.dev](https://vue-yup-form.pages.dev/). 25 | 26 | ## License 27 | MIT 28 | -------------------------------------------------------------------------------- /__tests__/field.spec.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue-demi"; 2 | import { testReactivity } from "./testReactivity"; 3 | 4 | import { field } from "../src/index"; 5 | import * as yup from "yup"; 6 | 7 | describe("field", () => { 8 | test("no schema was given", () => { 9 | const fieldObject = field(""); 10 | expect(fieldObject.$error).toBeUndefined(); 11 | }); 12 | 13 | test("given empty string when string $value is required", () => { 14 | const fieldObject = field("", yup.string().required()); 15 | expect(fieldObject.$error).toBeInstanceOf(yup.ValidationError); 16 | }); 17 | 18 | test("accept a Ref object other than shallowRef()", () => { 19 | const fieldObject = field( 20 | ref({ x: 0, y: 0, z: 0 }), 21 | yup.object().shape({ 22 | x: yup.number().required().integer(), 23 | y: yup.number().required().integer(), 24 | z: yup.number().required().integer(), 25 | }) 26 | ); 27 | expect(fieldObject.$error).toBeUndefined(); 28 | 29 | fieldObject.$value.x = 0.5; 30 | expect(fieldObject.$error).toBeInstanceOf(yup.ValidationError); 31 | }); 32 | 33 | test("abortEarly and $errorMessages", () => { 34 | const fieldObject1 = field("", yup.string().required().min(1), { 35 | abortEarly: true, 36 | }); 37 | expect(fieldObject1.$errorMessages).toHaveLength(1); 38 | 39 | const fieldObject2 = field("", yup.string().required().min(1), { 40 | abortEarly: false, 41 | }); 42 | expect(fieldObject2.$errorMessages).toHaveLength(2); 43 | 44 | const fieldObject3 = field("", yup.string(), { 45 | abortEarly: false, 46 | }); 47 | expect(fieldObject3.$errorMessages).toHaveLength(0); 48 | }); 49 | 50 | test("set empty string when number $value is required", () => { 51 | const REQUIRED_MESSAGE = "Must be a number!"; 52 | 53 | const fieldObject = field( 54 | 0, 55 | yup.number().required(REQUIRED_MESSAGE) 56 | ); 57 | expect(fieldObject.$error).toBeUndefined(); 58 | 59 | fieldObject.$value = null; 60 | expect(fieldObject.$error).toBeInstanceOf(yup.ValidationError); 61 | expect(fieldObject.$error?.message).toBe(REQUIRED_MESSAGE); 62 | 63 | // Changing `` to blank, `foo` is set to empty string. 64 | fieldObject.$value = "" as any; 65 | expect(fieldObject.$error).toBeInstanceOf(yup.ValidationError); 66 | expect(fieldObject.$error?.message).toBe(REQUIRED_MESSAGE); 67 | }); 68 | 69 | test("string array field", () => { 70 | const fieldObject = field([], yup.array().min(1)); 71 | expect(fieldObject.$error).toBeInstanceOf(yup.ValidationError); 72 | 73 | fieldObject.$value = ["apple"]; 74 | expect(fieldObject.$error).toBeUndefined(); 75 | }); 76 | 77 | test("$label", () => { 78 | const fieldObject1 = field("", yup.string().label("First Name")); 79 | expect(fieldObject1.$label).toBe("First Name"); 80 | 81 | const fieldObject2 = field("", yup.string()); 82 | expect(fieldObject2.$label).toBe(""); 83 | }); 84 | 85 | test("[reactivity] set $value", async () => { 86 | const fieldObject = field("foo", yup.string().required()); 87 | 88 | await expect( 89 | testReactivity(() => fieldObject.$value).trigger(() => { 90 | fieldObject.$value = "bar"; 91 | }) 92 | ).resolves.toBe("bar"); 93 | }); 94 | 95 | test("[reactivity] set $value and Field's $error", async () => { 96 | const fieldObject = field("", yup.string().required()); 97 | 98 | expect(fieldObject.$error).toBeInstanceOf(yup.ValidationError); 99 | 100 | await expect( 101 | testReactivity(() => fieldObject.$error).trigger(() => { 102 | fieldObject.$value = "foo"; 103 | }) 104 | ).resolves.toBeUndefined(); 105 | }); 106 | 107 | test("[reactivity] set $value and Field's $errorMessages", async () => { 108 | const fieldObject = field("", yup.string().required()); 109 | 110 | expect(fieldObject.$errorMessages).toHaveLength(1); 111 | 112 | await expect( 113 | testReactivity(() => fieldObject.$errorMessages).trigger(() => { 114 | fieldObject.$value = "foo"; 115 | }) 116 | ).resolves.toHaveLength(0); 117 | }); 118 | 119 | test("[reactivity] cross field validation", async () => { 120 | const fieldObject1 = field(false, yup.bool()); 121 | const fieldObject2 = field("", () => 122 | fieldObject1.$value ? yup.string().required() : yup.string() 123 | ); 124 | 125 | expect(fieldObject2.$error).toBeUndefined(); 126 | 127 | await expect( 128 | testReactivity(() => fieldObject2.$error).trigger(() => { 129 | fieldObject1.$value = true; 130 | }) 131 | ).resolves.toBeInstanceOf(yup.ValidationError); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /__tests__/formsField.spec.ts: -------------------------------------------------------------------------------- 1 | import { testReactivity } from "./testReactivity"; 2 | 3 | import { defineForm, field, formsField } from "../src/index"; 4 | import * as yup from "yup"; 5 | 6 | const generateFormWithNoParameter = () => 7 | defineForm({ 8 | text: field(""), 9 | }); 10 | 11 | const generateFormWithRequiredParameter = (initialText: string) => 12 | defineForm({ 13 | text: field(initialText), 14 | }); 15 | 16 | const generateFormWithOptionalParameter = (initialText = "default") => 17 | defineForm({ 18 | text: field(initialText), 19 | }); 20 | 21 | describe("formsField", () => { 22 | test("initialize formsField with generateForm function", () => { 23 | const formsFieldObject1 = formsField(generateFormWithNoParameter, 3); 24 | expect(formsFieldObject1.$forms[0]!.text.$value).toBe(""); 25 | expect(formsFieldObject1.$forms[1]!.text.$value).toBe(""); 26 | expect(formsFieldObject1.$forms[2]!.text.$value).toBe(""); 27 | 28 | const formsFieldObject2 = formsField(generateFormWithRequiredParameter, [ 29 | "A", 30 | "B", 31 | "C", 32 | ]); 33 | expect(formsFieldObject2.$forms[0]!.text.$value).toBe("A"); 34 | expect(formsFieldObject2.$forms[1]!.text.$value).toBe("B"); 35 | expect(formsFieldObject2.$forms[2]!.text.$value).toBe("C"); 36 | 37 | const formsFieldObject3 = formsField(generateFormWithOptionalParameter, 3); 38 | expect(formsFieldObject3.$forms[0]!.text.$value).toBe("default"); 39 | expect(formsFieldObject3.$forms[1]!.text.$value).toBe("default"); 40 | expect(formsFieldObject3.$forms[2]!.text.$value).toBe("default"); 41 | 42 | const formsFieldObject4 = formsField(generateFormWithOptionalParameter, [ 43 | "A", 44 | undefined, 45 | "C", 46 | ]); 47 | expect(formsFieldObject4.$forms[0]!.text.$value).toBe("A"); 48 | expect(formsFieldObject4.$forms[1]!.text.$value).toBe("default"); 49 | expect(formsFieldObject4.$forms[2]!.text.$value).toBe("C"); 50 | }); 51 | 52 | test("no schema was given", () => { 53 | const formsFieldObject = formsField(generateFormWithNoParameter); 54 | expect(formsFieldObject.$error).toBeUndefined(); 55 | }); 56 | 57 | test("static schema was given", () => { 58 | const formsFieldObject = formsField( 59 | generateFormWithNoParameter, 60 | 0, 61 | yup.array().min(1) 62 | ); 63 | expect(formsFieldObject.$error).toBeInstanceOf(yup.ValidationError); 64 | 65 | formsFieldObject.$append(); 66 | expect(formsFieldObject.$error).toBeUndefined(); 67 | }); 68 | 69 | test("functional schema was given", () => { 70 | const formsFieldObject = formsField( 71 | generateFormWithRequiredParameter, 72 | ["abcd", "efgh", "ij"], 73 | (arraySchema) => 74 | arraySchema.test({ 75 | test: (forms) => 76 | !!forms && 77 | forms.reduce( 78 | (prev, currentForm) => prev + currentForm.text.$value.length, 79 | 0 80 | ) <= 10, 81 | }) 82 | ); 83 | expect(formsFieldObject.$error).toBeUndefined(); 84 | 85 | formsFieldObject.$forms[2]!.text.$value += "k"; 86 | expect(formsFieldObject.$error).toBeInstanceOf(yup.ValidationError); 87 | }); 88 | 89 | test("$label", () => { 90 | const formsFieldObject1 = formsField( 91 | generateFormWithNoParameter, 92 | 0, 93 | yup.array().label("Questions") 94 | ); 95 | expect(formsFieldObject1.$label).toBe("Questions"); 96 | 97 | const formsFieldObject2 = formsField( 98 | generateFormWithNoParameter, 99 | 0, 100 | yup.array() 101 | ); 102 | expect(formsFieldObject2.$label).toBe(""); 103 | 104 | const formsFieldObject3 = formsField( 105 | generateFormWithNoParameter, 106 | 0, 107 | (arraySchema) => arraySchema.label("Questions") 108 | ); 109 | expect(formsFieldObject3.$label).toBe("Questions"); 110 | 111 | const formsFieldObject4 = formsField( 112 | generateFormWithNoParameter, 113 | 0, 114 | (arraySchema) => arraySchema 115 | ); 116 | expect(formsFieldObject4.$label).toBe(""); 117 | 118 | const formsFieldObject5 = formsField(generateFormWithNoParameter, 0); 119 | expect(formsFieldObject5.$label).toBe(""); 120 | }); 121 | 122 | test("abortEarly and $errorMessages", () => { 123 | const formsFieldObject1 = formsField( 124 | generateFormWithOptionalParameter, 125 | 2, 126 | yup.array().min(3).max(0), 127 | { 128 | abortEarly: true, 129 | } 130 | ); 131 | expect(formsFieldObject1.$errorMessages).toHaveLength(1); 132 | 133 | const formsFieldObject2 = formsField( 134 | generateFormWithOptionalParameter, 135 | 2, 136 | yup.array().min(3).max(0), 137 | { 138 | abortEarly: false, 139 | } 140 | ); 141 | expect(formsFieldObject2.$errorMessages).toHaveLength(2); 142 | 143 | const formsFieldObject3 = formsField( 144 | generateFormWithOptionalParameter, 145 | 0, 146 | yup.array(), 147 | { 148 | abortEarly: false, 149 | } 150 | ); 151 | expect(formsFieldObject3.$errorMessages).toHaveLength(0); 152 | }); 153 | 154 | test("form.$key properties are unique", () => { 155 | const formsFieldObject = formsField(generateFormWithNoParameter, 5); 156 | expect(formsFieldObject.$forms.map((form) => form.$key)).toEqual([ 157 | 1, 2, 3, 4, 5, 158 | ]); 159 | 160 | formsFieldObject.$prepend(); 161 | formsFieldObject.$append(); 162 | formsFieldObject.$append(); 163 | expect(formsFieldObject.$forms.map((form) => form.$key)).toEqual([ 164 | 6, 1, 2, 3, 4, 5, 7, 8, 165 | ]); 166 | }); 167 | 168 | test("[reactivity] set $writableForms", async () => { 169 | const formsFieldObject = formsField( 170 | generateFormWithNoParameter, 171 | 3, 172 | yup.array().min(1) 173 | ); 174 | 175 | expect(formsFieldObject.$forms.map((form) => form.$key)).toEqual([1, 2, 3]); 176 | 177 | await expect( 178 | testReactivity(() => 179 | formsFieldObject.$forms.map((form) => form.$key) 180 | ).trigger(() => { 181 | formsFieldObject.$writableForms = formsFieldObject.$writableForms 182 | .slice() 183 | .reverse(); 184 | }) 185 | ).resolves.toEqual([3, 2, 1]); 186 | }); 187 | 188 | test("parameter of $initialize()", async () => { 189 | const formsFieldObject = formsField(generateFormWithOptionalParameter); 190 | expect(formsFieldObject.$forms).toHaveLength(0); 191 | 192 | formsFieldObject.$initialize(["A", "B", "C"]); 193 | expect(formsFieldObject.$forms).toHaveLength(3); 194 | expect(formsFieldObject.$forms[0]!.text.$value).toBe("A"); 195 | expect(formsFieldObject.$forms[1]!.text.$value).toBe("B"); 196 | expect(formsFieldObject.$forms[2]!.text.$value).toBe("C"); 197 | 198 | formsFieldObject.$initialize(); 199 | expect(formsFieldObject.$forms).toHaveLength(0); 200 | 201 | formsFieldObject.$initialize(3); 202 | expect(formsFieldObject.$forms).toHaveLength(3); 203 | expect(formsFieldObject.$forms[0]!.text.$value).toBe("default"); 204 | expect(formsFieldObject.$forms[1]!.text.$value).toBe("default"); 205 | expect(formsFieldObject.$forms[2]!.text.$value).toBe("default"); 206 | }); 207 | test("parameter of $append()", async () => { 208 | const formsFieldObject = formsField(generateFormWithOptionalParameter); 209 | 210 | formsFieldObject.$append(); 211 | expect(formsFieldObject.$forms[0]!.text.$value).toBe("default"); 212 | 213 | formsFieldObject.$append("hello"); 214 | expect(formsFieldObject.$forms[1]!.text.$value).toBe("hello"); 215 | }); 216 | test("parameter of $prepend()", async () => { 217 | const formsFieldObject = formsField(generateFormWithOptionalParameter); 218 | 219 | formsFieldObject.$prepend(); 220 | expect(formsFieldObject.$forms[0]!.text.$value).toBe("default"); 221 | 222 | formsFieldObject.$prepend("hello"); 223 | expect(formsFieldObject.$forms[0]!.text.$value).toBe("hello"); 224 | }); 225 | test("parameter of $remove()", async () => { 226 | const formsFieldObject = formsField(generateFormWithOptionalParameter, 5); 227 | 228 | formsFieldObject.$remove(1); 229 | expect(formsFieldObject.$forms.map((form) => form.$key)).toEqual([ 230 | 1, 3, 4, 5, 231 | ]); 232 | 233 | formsFieldObject.$remove(-1); 234 | expect(formsFieldObject.$forms.map((form) => form.$key)).toEqual([1, 3, 4]); 235 | }); 236 | 237 | test("[reactivity] $initialize() and FormField's $forms", async () => { 238 | const formsFieldObject = formsField( 239 | generateFormWithNoParameter, 240 | 0, 241 | yup.array().min(1) 242 | ); 243 | 244 | expect(formsFieldObject.$forms).toHaveLength(0); 245 | 246 | await expect( 247 | testReactivity(() => formsFieldObject.$forms).trigger(() => { 248 | formsFieldObject.$initialize(1); 249 | }) 250 | ).resolves.toHaveLength(1); 251 | }); 252 | test("[reactivity] $append() and FormField's $forms", async () => { 253 | const formsFieldObject = formsField( 254 | generateFormWithNoParameter, 255 | 0, 256 | yup.array().min(1) 257 | ); 258 | 259 | expect(formsFieldObject.$forms).toHaveLength(0); 260 | 261 | await expect( 262 | testReactivity(() => formsFieldObject.$forms).trigger(() => { 263 | formsFieldObject.$append(); 264 | }) 265 | ).resolves.toHaveLength(1); 266 | }); 267 | test("[reactivity] $prepend() and FormField's $forms", async () => { 268 | const formsFieldObject = formsField( 269 | generateFormWithNoParameter, 270 | 0, 271 | yup.array().min(1) 272 | ); 273 | 274 | expect(formsFieldObject.$forms).toHaveLength(0); 275 | 276 | await expect( 277 | testReactivity(() => formsFieldObject.$forms).trigger(() => { 278 | formsFieldObject.$prepend(); 279 | }) 280 | ).resolves.toHaveLength(1); 281 | }); 282 | test("[reactivity] $remove(index) and FormField's $forms", async () => { 283 | const formsFieldObject = formsField( 284 | generateFormWithNoParameter, 285 | 2, 286 | yup.array().min(1) 287 | ); 288 | 289 | expect(formsFieldObject.$forms).toHaveLength(2); 290 | 291 | await expect( 292 | testReactivity(() => formsFieldObject.$forms).trigger(() => { 293 | formsFieldObject.$remove(0); 294 | }) 295 | ).resolves.toHaveLength(1); 296 | }); 297 | 298 | test("[reactivity] $initialize() and FormField's $error", async () => { 299 | const formsFieldObject = formsField( 300 | generateFormWithNoParameter, 301 | 0, 302 | yup.array().min(1) 303 | ); 304 | 305 | expect(formsFieldObject.$error).toBeInstanceOf(yup.ValidationError); 306 | 307 | await expect( 308 | testReactivity(() => formsFieldObject.$error).trigger(() => { 309 | formsFieldObject.$initialize(1); 310 | }) 311 | ).resolves.toBeUndefined(); 312 | }); 313 | test("[reactivity] $append() and FormField's $error", async () => { 314 | const formsFieldObject = formsField( 315 | generateFormWithNoParameter, 316 | 0, 317 | yup.array().min(1) 318 | ); 319 | 320 | expect(formsFieldObject.$error).toBeInstanceOf(yup.ValidationError); 321 | 322 | await expect( 323 | testReactivity(() => formsFieldObject.$error).trigger(() => { 324 | formsFieldObject.$append(); 325 | }) 326 | ).resolves.toBeUndefined(); 327 | }); 328 | test("[reactivity] $prepend() and FormField's $error", async () => { 329 | const formsFieldObject = formsField( 330 | generateFormWithNoParameter, 331 | 0, 332 | yup.array().min(1) 333 | ); 334 | 335 | expect(formsFieldObject.$error).toBeInstanceOf(yup.ValidationError); 336 | 337 | await expect( 338 | testReactivity(() => formsFieldObject.$error).trigger(() => { 339 | formsFieldObject.$prepend(); 340 | }) 341 | ).resolves.toBeUndefined(); 342 | }); 343 | test("[reactivity] $remove(index) and FormField's $error", async () => { 344 | const formsFieldObject = formsField( 345 | generateFormWithNoParameter, 346 | 1, 347 | yup.array().min(1) 348 | ); 349 | 350 | expect(formsFieldObject.$error).toBeUndefined(); 351 | 352 | await expect( 353 | testReactivity(() => formsFieldObject.$error).trigger(() => { 354 | formsFieldObject.$remove(0); 355 | }) 356 | ).resolves.toBeInstanceOf(yup.ValidationError); 357 | }); 358 | 359 | test("[reactivity] $initialize() and FormField's $errorMessages", async () => { 360 | const formsFieldObject = formsField( 361 | generateFormWithNoParameter, 362 | 0, 363 | yup.array().min(1) 364 | ); 365 | 366 | expect(formsFieldObject.$errorMessages).toHaveLength(1); 367 | 368 | await expect( 369 | testReactivity(() => formsFieldObject.$errorMessages).trigger(() => { 370 | formsFieldObject.$initialize(1); 371 | }) 372 | ).resolves.toHaveLength(0); 373 | }); 374 | test("[reactivity] $append() and FormField's $errorMessages", async () => { 375 | const formsFieldObject = formsField( 376 | generateFormWithNoParameter, 377 | 0, 378 | yup.array().min(1) 379 | ); 380 | 381 | expect(formsFieldObject.$errorMessages).toHaveLength(1); 382 | 383 | await expect( 384 | testReactivity(() => formsFieldObject.$errorMessages).trigger(() => { 385 | formsFieldObject.$append(); 386 | }) 387 | ).resolves.toHaveLength(0); 388 | }); 389 | test("[reactivity] $prepend() and FormField's $errorMessages", async () => { 390 | const formsFieldObject = formsField( 391 | generateFormWithNoParameter, 392 | 0, 393 | yup.array().min(1) 394 | ); 395 | 396 | expect(formsFieldObject.$errorMessages).toHaveLength(1); 397 | 398 | await expect( 399 | testReactivity(() => formsFieldObject.$errorMessages).trigger(() => { 400 | formsFieldObject.$prepend(); 401 | }) 402 | ).resolves.toHaveLength(0); 403 | }); 404 | test("[reactivity] $remove(index) and FormField's $errorMessages", async () => { 405 | const formsFieldObject = formsField( 406 | generateFormWithNoParameter, 407 | 1, 408 | yup.array().min(1) 409 | ); 410 | 411 | expect(formsFieldObject.$errorMessages).toHaveLength(0); 412 | 413 | await expect( 414 | testReactivity(() => formsFieldObject.$errorMessages).trigger(() => { 415 | formsFieldObject.$remove(0); 416 | }) 417 | ).resolves.toHaveLength(1); 418 | }); 419 | 420 | test("[reactivity] cross field validation", async () => { 421 | const fieldObject = field(false, yup.bool()); 422 | const formsFieldObject = formsField(generateFormWithNoParameter, 0, () => 423 | fieldObject.$value ? yup.array().min(1) : yup.array() 424 | ); 425 | expect(formsFieldObject.$error).toBeUndefined(); 426 | 427 | fieldObject.$value = true; 428 | expect(formsFieldObject.$error).toBeInstanceOf(yup.ValidationError); 429 | }); 430 | }); 431 | -------------------------------------------------------------------------------- /__tests__/isValidForm.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineForm, 3 | field, 4 | privateField, 5 | formsField, 6 | isValidForm, 7 | } from "../src/index"; 8 | import * as yup from "yup"; 9 | 10 | describe("isValidForm", () => { 11 | test("basic form", () => { 12 | const form = defineForm({ 13 | a: field("A", yup.string().required()), 14 | b: field("", yup.string().required()), 15 | c: field("C", yup.string().required()), 16 | id: privateField(null, yup.number().required()), 17 | memo: privateField("hello, world"), 18 | method() {}, 19 | }); 20 | expect(isValidForm(form)).toBe(false); 21 | 22 | form.b.$value = "B"; 23 | expect(isValidForm(form)).toBe(false); 24 | 25 | form.id.$value = 1; 26 | expect(isValidForm(form)).toBe(true); 27 | }); 28 | 29 | test("form includes nested forms", () => { 30 | const form = defineForm({ 31 | a: { 32 | a: field("A", yup.string().required()), 33 | b: { 34 | a: field("", yup.string().required()), 35 | }, 36 | c: field("C", yup.string().required()), 37 | }, 38 | b: field("B", yup.string().required()), 39 | }); 40 | expect(isValidForm(form)).toBe(false); 41 | expect(isValidForm(form.a)).toBe(false); 42 | expect(isValidForm(form.a.b)).toBe(false); 43 | 44 | form.a.b.a.$value = "A"; 45 | expect(isValidForm(form)).toBe(true); 46 | expect(isValidForm(form.a)).toBe(true); 47 | expect(isValidForm(form.a.b)).toBe(true); 48 | 49 | form.a.c.$value = ""; 50 | expect(isValidForm(form)).toBe(false); 51 | expect(isValidForm(form.a)).toBe(false); 52 | expect(isValidForm(form.a.b)).toBe(true); 53 | }); 54 | 55 | test("form includes formsField", () => { 56 | const generateSubForm = (initialText = "") => 57 | defineForm({ 58 | text: field(initialText, yup.string().required()), 59 | }); 60 | 61 | const form = defineForm({ 62 | a: formsField(generateSubForm, 0, yup.array().min(1)), 63 | b: field("B", yup.string().required()), 64 | }); 65 | expect(isValidForm(form)).toBe(false); 66 | 67 | form.a.$append("hello"); 68 | expect(isValidForm(form)).toBe(true); 69 | 70 | form.a.$append(); 71 | expect(isValidForm(form)).toBe(false); 72 | 73 | form.a.$forms[1]!.text.$value = "world"; 74 | expect(isValidForm(form)).toBe(true); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/testReactivity.ts: -------------------------------------------------------------------------------- 1 | import { watchEffect, nextTick } from "vue-demi"; 2 | 3 | class TestReactivity { 4 | constructor(private target: () => T) {} 5 | 6 | public trigger(fn: () => void): Promise { 7 | return new Promise((resolve) => { 8 | let valueToBeResolved: T; 9 | 10 | watchEffect(() => { 11 | valueToBeResolved = this.target(); 12 | }); 13 | 14 | fn(); 15 | 16 | nextTick(() => { 17 | resolve(valueToBeResolved); 18 | }); 19 | }); 20 | } 21 | } 22 | 23 | export const testReactivity = (target: () => T) => 24 | new TestReactivity(target); 25 | -------------------------------------------------------------------------------- /__tests__/toObject.spec-d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineForm, 3 | field, 4 | privateField, 5 | formsField, 6 | toObject, 7 | } from "../src/index"; 8 | import * as yup from "yup"; 9 | 10 | describe("toObject", () => { 11 | test("basic form", () => { 12 | const form = defineForm({ 13 | a: field("A", yup.string().required()), 14 | b: field("", yup.string().required()), 15 | c: field("C", yup.string().required()), 16 | id: privateField(null, yup.number().required()), 17 | memo: privateField("hello, world"), 18 | method() {}, 19 | }); 20 | expectTypeOf(toObject(form)).toEqualTypeOf<{ 21 | a: string; 22 | b: string; 23 | c: string; 24 | }>(); 25 | }); 26 | 27 | test("form includes nested forms", () => { 28 | const form = defineForm({ 29 | a: { 30 | a: field("A", yup.string().required()), 31 | b: { 32 | a: field("", yup.string().required()), 33 | }, 34 | c: field("C", yup.string().required()), 35 | }, 36 | b: field("B", yup.string().required()), 37 | }); 38 | expectTypeOf(toObject(form)).toEqualTypeOf<{ 39 | a: { 40 | a: string; 41 | b: { 42 | a: string; 43 | }; 44 | c: string; 45 | }; 46 | b: string; 47 | }>(); 48 | expectTypeOf(toObject(form.a)).toEqualTypeOf<{ 49 | a: string; 50 | b: { 51 | a: string; 52 | }; 53 | c: string; 54 | }>(); 55 | expectTypeOf(toObject(form.a.b)).toEqualTypeOf<{ 56 | a: string; 57 | }>(); 58 | }); 59 | 60 | test("form includes FieldWithPreferredType", () => { 61 | type Foo = "A" | "B" | "C"; 62 | 63 | const form = defineForm({ 64 | a: field("A", yup.string().required()), 65 | b: field("A", yup.string().required()), 66 | c: field("A", yup.string().required()), 67 | }); 68 | expectTypeOf(toObject(form)).toEqualTypeOf<{ 69 | a: string; 70 | b: Foo | null; 71 | c: Foo; 72 | }>(); 73 | }); 74 | 75 | test("form includes formsField", () => { 76 | const generateSubForm = (initialText = "") => 77 | defineForm({ 78 | text: field(initialText, yup.string().required()), 79 | }); 80 | 81 | const form = defineForm({ 82 | a: formsField(generateSubForm, 1, yup.array().min(1)), 83 | b: field("B", yup.string().required()), 84 | }); 85 | expectTypeOf(toObject(form)).toEqualTypeOf<{ 86 | a: { text: string }[]; 87 | b: string; 88 | }>(); 89 | expectTypeOf(toObject(form.a.$forms[0]!)).toEqualTypeOf<{ 90 | text: string; 91 | }>(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /__tests__/toObject.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineForm, 3 | field, 4 | privateField, 5 | formsField, 6 | toObject, 7 | } from "../src/index"; 8 | import * as yup from "yup"; 9 | 10 | describe("toObject", () => { 11 | test("basic form", () => { 12 | const form = defineForm({ 13 | a: field("A", yup.string().required()), 14 | b: field("", yup.string().required()), 15 | c: field("C", yup.string().required()), 16 | id: privateField(null, yup.number().required()), 17 | memo: privateField("hello, world"), 18 | method() {}, 19 | }); 20 | expect(toObject(form)).toEqual({ 21 | a: "A", 22 | b: "", 23 | c: "C", 24 | }); 25 | }); 26 | 27 | test("form includes nested forms", () => { 28 | const form = defineForm({ 29 | a: { 30 | a: field("A", yup.string().required()), 31 | b: { 32 | a: field("", yup.string().required()), 33 | }, 34 | c: field("C", yup.string().required()), 35 | }, 36 | b: field("B", yup.string().required()), 37 | }); 38 | expect(toObject(form)).toEqual({ 39 | a: { 40 | a: "A", 41 | b: { 42 | a: "", 43 | }, 44 | c: "C", 45 | }, 46 | b: "B", 47 | }); 48 | expect(toObject(form.a)).toEqual({ 49 | a: "A", 50 | b: { 51 | a: "", 52 | }, 53 | c: "C", 54 | }); 55 | expect(toObject(form.a.b)).toEqual({ 56 | a: "", 57 | }); 58 | }); 59 | 60 | test("form includes formsField", () => { 61 | const generateSubForm = (initialText = "") => 62 | defineForm({ 63 | text: field(initialText, yup.string().required()), 64 | }); 65 | 66 | const form = defineForm({ 67 | a: formsField(generateSubForm, 0, yup.array().min(1)), 68 | b: field("B", yup.string().required()), 69 | }); 70 | expect(toObject(form)).toEqual({ 71 | a: [], 72 | b: "B", 73 | }); 74 | 75 | form.a.$append("hello"); 76 | expect(toObject(form)).toEqual({ 77 | a: [{ text: "hello" }], 78 | b: "B", 79 | }); 80 | 81 | form.a.$append(); 82 | expect(toObject(form)).toEqual({ 83 | a: [{ text: "hello" }, { text: "" }], 84 | b: "B", 85 | }); 86 | 87 | form.a.$forms[1]!.text.$value = "world"; 88 | expect(toObject(form)).toEqual({ 89 | a: [{ text: "hello" }, { text: "world" }], 90 | b: "B", 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /config/vitest.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | typecheck: { 7 | ignoreSourceErrors: true, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | 3 | import { version } from "../../package.json"; 4 | 5 | export default defineConfig({ 6 | title: "vue-yup-form", 7 | description: "Headless form validation with Vue and Yup", 8 | themeConfig: { 9 | logo: "/logo.svg", 10 | sidebar: sidebarGuide(), 11 | search: { provider: "local" }, 12 | nav: [ 13 | { 14 | text: `v${version}`, 15 | items: [ 16 | { 17 | text: "Release Notes", 18 | link: "https://github.com/mascii/vue-yup-form/releases", 19 | }, 20 | ], 21 | }, 22 | ], 23 | socialLinks: [ 24 | { icon: "github", link: "https://github.com/mascii/vue-yup-form" }, 25 | ], 26 | editLink: { 27 | pattern: "https://github.com/mascii/vue-yup-form/tree/main/docs/:path", 28 | text: "Suggest changes to this page", 29 | }, 30 | }, 31 | head: [ 32 | ["link", { rel: "icon", href: "/favicon-32x32.png", type: "image/png" }], 33 | ["link", { rel: "icon", href: "/logo.svg", type: "image/svg+xml" }], 34 | ["meta", { name: "author", content: "mascii" }], 35 | ["meta", { property: "og:title", content: "vue-yup-form" }], 36 | [ 37 | "meta", 38 | { 39 | property: "og:image", 40 | content: "https://vue-yup-form.pages.dev/og.png", 41 | }, 42 | ], 43 | [ 44 | "meta", 45 | { 46 | property: "og:description", 47 | content: "Headless form validation with Vue and Yup", 48 | }, 49 | ], 50 | ["meta", { name: "twitter:card", content: "summary_large_image" }], 51 | ["meta", { name: "twitter:creator", content: "@mascii_k" }], 52 | [ 53 | "meta", 54 | { 55 | name: "twitter:image", 56 | content: "https://vue-yup-form.pages.dev/og.png", 57 | }, 58 | ], 59 | ], 60 | }); 61 | 62 | function sidebarGuide() { 63 | return [ 64 | { 65 | text: "Guide", 66 | collapsible: true, 67 | items: [{ text: "Getting Started", link: "/guide/getting-started" }], 68 | }, 69 | { 70 | text: "Examples", 71 | collapsible: true, 72 | items: [ 73 | { text: "Simple Example", link: "/examples/simple-example" }, 74 | { 75 | text: "Checkbox and Radio Inputs", 76 | link: "/examples/checkboxes-and-radio", 77 | }, 78 | { 79 | text: "Cross-Field Validation", 80 | link: "/examples/cross-field-validation", 81 | }, 82 | { text: "Array of forms", link: "/examples/array-of-forms" }, 83 | { 84 | text: "Array of forms with Vue.Draggable", 85 | link: "/examples/array-of-forms-with-vue-draggable", 86 | }, 87 | { 88 | text: "Dividing form components", 89 | link: "/examples/dividing-form-components", 90 | }, 91 | { 92 | text: "Multi-step form wizard", 93 | link: "/examples/multi-step-form-wizard", 94 | }, 95 | { 96 | text: "Pseudo async validation", 97 | link: "/examples/pseudo-async-validation", 98 | }, 99 | ], 100 | }, 101 | { 102 | text: "APIs", 103 | collapsible: true, 104 | items: [ 105 | { text: "defineForm()", link: "/api/defineForm" }, 106 | { text: "field()", link: "/api/field" }, 107 | { text: "privateField()", link: "/api/privateField" }, 108 | { text: "formsField()", link: "/api/formsField" }, 109 | { text: "isValidForm()", link: "/api/isValidForm" }, 110 | { text: "toObject()", link: "/api/toObject" }, 111 | ], 112 | }, 113 | { 114 | text: "Types", 115 | collapsible: true, 116 | items: [ 117 | { text: "yup.ValidationError", link: "/types/YupValidationError" }, 118 | { text: "FormPropType", link: "/types/FormPropType" }, 119 | ], 120 | }, 121 | ]; 122 | } 123 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | 3 | import { ref } from "vue"; 4 | import { version as vueVersion } from "vue/package.json"; 5 | import * as yup from "yup"; 6 | import { version as yupVersion } from "yup/package.json"; 7 | import * as vueYupForm from "../../../src/index"; 8 | import { version as vueYupFormVersion } from "../../../package.json"; 9 | 10 | import "./styles/vars.css"; 11 | 12 | import StackBlitz from "../../components/StackBlitz.vue"; 13 | 14 | Object.assign(globalThis, { ref }, { yup }, vueYupForm); 15 | 16 | console.info( 17 | "%cFeel free to try sample codes here", 18 | "background: #222; color: #EE6A55" 19 | ); 20 | console.info(`vue version: ${vueVersion}`); 21 | console.info(`yup version: ${yupVersion}`); 22 | console.info(`vue-yup-form version: ${vueYupFormVersion}`); 23 | 24 | export default { 25 | ...DefaultTheme, 26 | enhanceApp({ app }) { 27 | DefaultTheme.enhanceApp.apply(this, arguments); 28 | app.component("StackBlitz", StackBlitz); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Component: Home 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-home-hero-name-color: transparent; 7 | --vp-home-hero-name-background: -webkit-linear-gradient( 8 | 120deg, 9 | #35495e -80%, 10 | #41b883 11 | ); 12 | --vp-home-hero-image-background-image: linear-gradient( 13 | -45deg, 14 | #41b88380 30%, 15 | #35495e80 16 | ); 17 | --vp-home-hero-image-filter: blur(30px); 18 | } 19 | 20 | @media (min-width: 640px) { 21 | :root { 22 | --vp-home-hero-image-filter: blur(56px); 23 | } 24 | } 25 | 26 | @media (min-width: 960px) { 27 | :root { 28 | --vp-home-hero-image-filter: blur(72px); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/api/defineForm.md: -------------------------------------------------------------------------------- 1 | # defineForm() 2 | An identity function whose argument type extends the `Form` type. This function is used for type checking. 3 | 4 | ```typescript 5 | function defineForm(form: T): T { 6 | return form; 7 | } 8 | ``` 9 | 10 | ## Details of `Form` type 11 | An object that satisfies the following: 12 | 13 | - Key 14 | - Starting with `$` are not available. 15 | - Value is one of the following 16 | - `Field` object ([`field()`](/api/field) returns) 17 | - `PrivateField` object ([`privateField()`](/api/privateField) returns) 18 | - `FormsField` object ([`formsField()`](/api/formsField) returns) 19 | - A Function 20 | - `Form` object recursively 21 | 22 | ```typescript 23 | type Form = { 24 | [key: `$${string}`]: never; 25 | [key: string]: 26 | | Field 27 | | PrivateField 28 | | FormsField<(arg: any) => Form> 29 | | ((...args: any[]) => any) 30 | | Form; 31 | }; 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/api/field.md: -------------------------------------------------------------------------------- 1 | # field() 2 | This function takes an initial value and a yup schema. 3 | This function returns `Field` object. 4 | 5 | ## Parameters 6 | The `field()` takes 3 parameters and some generic type parameters: 7 | 8 | ```typescript 9 | declare function field( 10 | value: T | Ref, 11 | schema?: FieldSchema, 12 | validateOptions?: ValidateOptions 13 | ): Field; 14 | declare function field( 15 | value: T | Ref, 16 | schema?: FieldSchema, 17 | validateOptions?: ValidateOptions 18 | ): FieldWithPreferredType; 19 | ``` 20 | 21 | ### 1. `value` 22 | - Pass an initial value like such as `string`, `number`, `boolean`, and `string[]` types. 23 | 24 | ::: tip 25 | Internally the `value` is made reactive by `shallowRef()`. 26 | To use a Ref other than `shallowRef()`, pass a Ref object instead. 27 | 28 | ```typescript 29 | import { ref } from "vue"; 30 | 31 | const point = field( 32 | ref({ x: 0, y: 0, z: 0 }), 33 | yup 34 | .object() 35 | .shape({ x: yup.number().required(), y: yup.number().required(), z: yup.number().required() }) 36 | .test({ 37 | test: ({ x, y, z }) => x ** 2 + y ** 2 + z ** 2 <= 1, 38 | message: "Specify the inside of the unit ball.", 39 | }) 40 | ); 41 | ``` 42 | ::: 43 | 44 | ### 2. `schema` 45 | - Pass a yup schema or a function that returns a yup schema. 46 | - If you pass a function, you can refer to `$value` of other fields. 47 | 48 | ::: tip 49 | Even when using `v-model.number`, changing the input box to empty may set `""` (an empty string). 50 | This causes the validation with `yup.NumberSchema` to output an undesirable error message: 51 | 52 | ```typescript 53 | try { 54 | yup.number().required().validateSync(""); 55 | } catch (e) { 56 | console.log(e.message); // this must be a `number` type, but the final value was: `NaN` (cast from the value `""`). 57 | } 58 | ``` 59 | 60 | To avoid the above, empty strings are validated as `undefined` if `yup.NumberSchema` is specified. 61 | 62 | ```typescript 63 | const age = field(20, yup.number().required()); 64 | 65 | // Equivalent to changing to empty 66 | age.$value = ""; 67 | 68 | console.log(age.$error.message); // this is a required field 69 | ``` 70 | ::: 71 | 72 | ::: warning 73 | Since `validateSync()` is used internally, Async tests are not available. 74 | ::: 75 | 76 | ### 3. `validateOptions` 77 | - Pass `{ abortEarly: false }` if all validation errors by the yup schema are required. 78 | 79 | ### `T` 80 | - Pass the type of `$value` explicitly like `ref()`. 81 | 82 | ### `U` 83 | - Pass the preferred type to be the result of [`toObject()`](/api/toObject). 84 | 85 | ## Details of `Field` object 86 | ### `$value` property 87 | - The field value that can be set to `v-model`. 88 | 89 | ### `$error` property 90 | - Returns a result of validating `$value` reactively with the yup schema. 91 | - If `$value` is invalid, `yup.ValidationError` is returned. 92 | - If `$value` is valid, `undefined` is returned. 93 | 94 | ### `$errorMessages` property 95 | - Returns an error messages array (`string[]`) reactively as well as `$error` property. 96 | - If `abortEarly` is false, it can return more than one element. 97 | 98 | ### `$label` property 99 | - Returns the label of the yup schema. 100 | - For example, if you pass `yup.number().label("Age")`, you will obtain `"Age"`. 101 | - If the `schema` you passed is a function, the function is evaluated once when the object is created. 102 | -------------------------------------------------------------------------------- /docs/api/formsField.md: -------------------------------------------------------------------------------- 1 | # formsField() 2 | This function is used to create an array of forms. 3 | This function returns `FormsField` object. 4 | 5 | ## Parameters 6 | The `formsField()` takes 4 parameters: 7 | 8 | ```typescript 9 | declare function formsField Form>( 10 | generateForm: T, 11 | initialValueListOrLength: InitialValueListOrLength = [], 12 | schema?: FormsFieldSchema>, 13 | validateOptions?: ValidateOptions 14 | ): FormsField; 15 | ``` 16 | 17 | ### 1. `generateForm` 18 | - Pass a function that returns a form with 0 or 1 parameter. 19 | - A form should be defined using [`defineForm()`](/api/defineForm). 20 | 21 | ### 2. `initialValueListOrLength` 22 | - Pass an array to initialize forms using `generateForm`. 23 | - Each element of the passed array is used as the parameter of `generateForm`. 24 | - If the parameter of `generateForm` is optional, you can also pass an integer greater than or equal to 0 instead of an array. 25 | - `[]` and `0` have equivalent meaning. 26 | 27 | ### 3. `schema` 28 | - Pass a yup *array* schema or a function that returns a yup *array* schema. 29 | - If you pass a function, the first argument is a *type bound* `yup.ArraySchema` object. 30 | - The same type as `$forms` is available in the `test()` method of `yup.ArraySchema`. 31 | - If you pass a function, you can refer to `$value` of other fields. 32 | - For example, use to check that the length of `$forms` is greater than 0. 33 | 34 | ### 4. `validateOptions` 35 | - Pass `{ abortEarly: false }` if all validation errors by the yup array schema are required. 36 | 37 | ## Details of `FormsField` object 38 | ### `$forms` property 39 | - Returns the array of forms generated by `generateForm`. 40 | - For use with `v-for`, each generated form is given a unique key named `$key`. 41 | 42 | ### `$error` property 43 | - Returns a result of validating `$forms` reactively with the yup schema. 44 | - If `$forms` is invalid, `yup.ValidationError` is returned. 45 | - If `$forms` is valid, `undefined` is returned. 46 | 47 | ### `$errorMessages` property 48 | - Returns an error messages array (`string[]`) reactively as well as `$error` property. 49 | - If `abortEarly` is false, it can return more than one element. 50 | 51 | ### `$label` property 52 | - Returns the label of the yup schema. 53 | - For example, if you pass `yup.array().label("Questions")`, you will obtain `"Questions"`. 54 | - If the `schema` you passed is a function, the function is evaluated once when the object is created. 55 | 56 | ### `$initialize()` method 57 | - Pass an array to initialize forms using `generateForm`. 58 | - The parameter of this method is the same as the second parameter of `formsField()`. 59 | - If the argument is omitted, all array elements will be removed. 60 | 61 | ### `$append()` method 62 | - Pass a parameter to initialize a form using `generateForm`, and the result is appended to `$forms`. 63 | - If the parameter of `generateForm` is optional, you can omit the argument. 64 | 65 | ### `$prepend()` method 66 | - Pass a parameter to initialize a form using `generateForm`, and the result is prepended to `$forms`. 67 | - If the parameter of `generateForm` is optional, you can omit the argument. 68 | 69 | ### `$remove()` method 70 | - Pass the index of the array element to be removed. 71 | - Negative integers count back from the last item in the array, like [`Array.prototype.at`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at). 72 | 73 | ### `$writableForms` property 74 | - :warning: **Usually should not be used.** 75 | - Even if you use the [`Array.prototype.push`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) method etc, `$error` property cannot detect the change by reactively. 76 | - Returns the same reference as `$forms` property, and settable. 77 | - For example, using with [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)'s draggable component. 78 | - `` 79 | -------------------------------------------------------------------------------- /docs/api/isValidForm.md: -------------------------------------------------------------------------------- 1 | # isValidForm() 2 | This function takes a form and checks that all `$error` properties in the form are `undefined`. 3 | This function returns boolean. 4 | 5 | ## Parameters 6 | The `isValidForm()` takes 1 parameter: 7 | 8 | ```typescript 9 | declare function isValidForm( 10 | form: T | (T & { $key: number }) 11 | ): boolean; 12 | ``` 13 | 14 | ### 1. `forms` 15 | - A form should be defined using [`defineForm()`](/api/defineForm). 16 | -------------------------------------------------------------------------------- /docs/api/privateField.md: -------------------------------------------------------------------------------- 1 | # privateField() 2 | This function can be used in the same way as [`field()`](/api/field), but is not included in the result of [`toObject()`](/api/toObject). -------------------------------------------------------------------------------- /docs/api/toObject.md: -------------------------------------------------------------------------------- 1 | # toObject() 2 | This function takes a form and returns unwrapped `Field` and `FormsField` contained in the form. 3 | This function does not check if the form is valid. 4 | 5 | ## Parameters 6 | The `toObject()` takes 1 parameter: 7 | 8 | ```typescript 9 | declare function toObject( 10 | form: T | (T & { $key: number }) 11 | ): Expand>; 12 | ``` 13 | 14 | ### 1. `forms` 15 | - A form should be defined using [`defineForm()`](/api/defineForm). 16 | 17 | ## Example 18 | ```typescript 19 | test("toObject()", () => { 20 | const generateSubForm = (initialText = "") => 21 | defineForm({ 22 | text: field(initialText, yup.string().required()), 23 | }); 24 | 25 | const form = defineForm({ 26 | a: { 27 | a: field("A", yup.string().required()), 28 | b: { 29 | a: field("", yup.string().required()), 30 | }, 31 | c: field("C", yup.string().required()), 32 | }, 33 | b: field("B", yup.string().required()), 34 | c: privateField("C", yup.string().required()), 35 | d: formsField(generateSubForm, ["x", "y", ""], yup.array().min(1)), 36 | e: () => {}, 37 | }); 38 | 39 | expect(toObject(form)).toEqual({ 40 | a: { 41 | a: "A", 42 | b: { 43 | a: "", 44 | }, 45 | c: "C", 46 | }, 47 | b: "B", 48 | d: [{ text: "x" }, { text: "y" }, { text: "" }], 49 | }); 50 | }); 51 | ``` -------------------------------------------------------------------------------- /docs/components/StackBlitz.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 46 | 47 | 62 | -------------------------------------------------------------------------------- /docs/examples/array-of-forms-with-vue-draggable.md: -------------------------------------------------------------------------------- 1 | # Array of forms with Vue.Draggable 2 | This example shows how to implement an array of forms with [`formsField()`](/api/formsField) and [vue.draggable.next 3 | ](https://github.com/SortableJS/vue.draggable.next). 4 | 5 | 6 | 7 | This example is based on [Examples / Array of forms](/examples/array-of-forms). 8 | -------------------------------------------------------------------------------- /docs/examples/array-of-forms.md: -------------------------------------------------------------------------------- 1 | # Array of forms 2 | This example shows how to implement an array of forms with [`formsField()`](/api/formsField). 3 | 4 | 5 | 6 | This example is based on [EXAMPLES / Array Fields](https://vee-validate.logaretm.com/v4/examples/array-fields) of VeeValidate docs. 7 | -------------------------------------------------------------------------------- /docs/examples/checkboxes-and-radio.md: -------------------------------------------------------------------------------- 1 | # Checkbox and Radio Inputs 2 | These examples show how to implement forms with HTML checkboxes and radio inputs. 3 | 4 | ## Validating Radio Inputs 5 | 6 | 7 | This example is based on [EXAMPLES / Checkbox and Radio Inputs](https://vee-validate.logaretm.com/v4/examples/checkboxes-and-radio#validating-radio-inputs) of VeeValidate docs. 8 | 9 | ## Validating Checkbox Inputs 10 | 11 | 12 | This example is based on [EXAMPLES / Checkbox and Radio Inputs](https://vee-validate.logaretm.com/v4/examples/checkboxes-and-radio#validating-radio-inputs) of VeeValidate docs. 13 | -------------------------------------------------------------------------------- /docs/examples/cross-field-validation.md: -------------------------------------------------------------------------------- 1 | # Cross-Field Validation 2 | This example shows how to create a password-confirmation-like rules. 3 | 4 | 5 | 6 | This example is based on [EXAMPLES / Cross-Field Validation](https://vee-validate.logaretm.com/v4/examples/cross-field-validation) of VeeValidate docs. 7 | -------------------------------------------------------------------------------- /docs/examples/dividing-form-components.md: -------------------------------------------------------------------------------- 1 | # Dividing form components 2 | This example shows how to divide form components with [`FormPropType`](/types/FormPropType). 3 | 4 | 5 | 6 | This example is based on [Examples / Array of forms](/examples/array-of-forms). 7 | -------------------------------------------------------------------------------- /docs/examples/multi-step-form-wizard.md: -------------------------------------------------------------------------------- 1 | # Multi-step form wizard 2 | This example shows a simple multi-step form (form wizard), with step navigation. 3 | 4 | 5 | 6 | This example is based on [EXAMPLES / Multi-step Form Wizard](https://vee-validate.logaretm.com/v4/examples/multistep-form-wizard) of VeeValidate docs. 7 | -------------------------------------------------------------------------------- /docs/examples/pseudo-async-validation.md: -------------------------------------------------------------------------------- 1 | # Pseudo async validation 2 | This example shows how to implement a form confirms that the e-mail address is not registered. 3 | 4 | ::: warning 5 | Since `validateSync()` is used internally, Async tests cannot be included as a yup schema. 6 | ::: 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/simple-example.md: -------------------------------------------------------------------------------- 1 | # Simple Example 2 | This example shows how to implement a form with ``, `