├── .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 |
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 |
40 |
45 |
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 ``, `