0
# Collections and Nested Validation
1
2
Advanced validation features for arrays, nested objects, and dynamic collections with support for tracking, conditional validation, and complex data structures.
3
4
## Capabilities
5
6
### Array Validation with $each
7
8
Validation of array elements using the special `$each` key for dynamic collections.
9
10
```javascript { .api }
11
/**
12
* Collection validation configuration for arrays
13
*/
14
interface CollectionValidation {
15
$each: {
16
[validationKey: string]: ValidationRule;
17
$trackBy?: string | ((item: any) => string);
18
};
19
}
20
21
/**
22
* Validation state for collections includes $iter property
23
*/
24
interface CollectionValidationState extends ValidationState {
25
/**
26
* Iterator object providing access to individual element validations
27
*/
28
readonly $iter: {
29
[index: string]: ValidationState;
30
};
31
}
32
```
33
34
**Usage:**
35
36
```javascript
37
import { required, email, minLength } from 'vuelidate/lib/validators'
38
39
export default {
40
data() {
41
return {
42
users: [
43
{ name: 'Alice', email: 'alice@example.com' },
44
{ name: 'Bob', email: 'bob@example.com' }
45
]
46
}
47
},
48
49
validations: {
50
users: {
51
$each: {
52
name: { required, minLength: minLength(2) },
53
email: { required, email }
54
}
55
}
56
},
57
58
computed: {
59
// Access validation for specific array elements
60
firstUserErrors() {
61
return this.$v.users.$iter[0]
62
},
63
64
// Check if any user has validation errors
65
hasUserErrors() {
66
return this.$v.users.$error
67
}
68
},
69
70
methods: {
71
// Add new user with automatic validation
72
addUser() {
73
this.users.push({ name: '', email: '' })
74
// Validation automatically extends to new items
75
},
76
77
// Remove user (validation automatically adjusts)
78
removeUser(index) {
79
this.users.splice(index, 1)
80
},
81
82
// Validate all users
83
validateAllUsers() {
84
this.$v.users.$touch()
85
return !this.$v.users.$invalid
86
}
87
}
88
}
89
```
90
91
### Tracking with $trackBy
92
93
Stable tracking of array elements during reordering and modifications.
94
95
```javascript { .api }
96
/**
97
* Track array elements by a specific property or function
98
*/
99
interface TrackingOptions {
100
$trackBy: string | ((item: any) => string);
101
}
102
```
103
104
**Usage:**
105
106
```javascript
107
export default {
108
data() {
109
return {
110
todos: [
111
{ id: 1, text: 'Learn Vuelidate', completed: false },
112
{ id: 2, text: 'Build awesome app', completed: false }
113
]
114
}
115
},
116
117
validations: {
118
todos: {
119
$each: {
120
// Track by ID to maintain validation state during reordering
121
$trackBy: 'id',
122
text: { required, minLength: minLength(3) }
123
}
124
}
125
},
126
127
methods: {
128
// Reorder todos - validation state follows the tracked items
129
moveTodoUp(index) {
130
if (index > 0) {
131
const item = this.todos.splice(index, 1)[0]
132
this.todos.splice(index - 1, 0, item)
133
// Validation state is preserved due to $trackBy
134
}
135
},
136
137
// Dynamic tracking based on content
138
validationsWithDynamicTracking() {
139
return {
140
items: {
141
$each: {
142
$trackBy: (item) => `${item.category}-${item.id}`,
143
name: { required }
144
}
145
}
146
}
147
}
148
}
149
}
150
```
151
152
### Nested Object Validation
153
154
Validation of nested object structures with hierarchical validation state.
155
156
```javascript { .api }
157
/**
158
* Nested validation configuration
159
*/
160
interface NestedValidation {
161
[propertyName: string]: {
162
[validationKey: string]: ValidationRule | NestedValidation;
163
};
164
}
165
```
166
167
**Usage:**
168
169
```javascript
170
export default {
171
data() {
172
return {
173
user: {
174
personal: {
175
firstName: '',
176
lastName: '',
177
birthDate: ''
178
},
179
contact: {
180
email: '',
181
phone: '',
182
address: {
183
street: '',
184
city: '',
185
postalCode: ''
186
}
187
}
188
}
189
}
190
},
191
192
validations: {
193
user: {
194
personal: {
195
firstName: { required, minLength: minLength(2) },
196
lastName: { required, minLength: minLength(2) },
197
birthDate: { required }
198
},
199
contact: {
200
email: { required, email },
201
phone: { required },
202
address: {
203
street: { required },
204
city: { required },
205
postalCode: { required, minLength: minLength(5) }
206
}
207
}
208
}
209
},
210
211
computed: {
212
// Access nested validation states
213
personalInfoValid() {
214
return !this.$v.user.personal.$invalid
215
},
216
217
addressValid() {
218
return !this.$v.user.contact.address.$invalid
219
},
220
221
contactSectionErrors() {
222
const contact = this.$v.user.contact
223
return {
224
hasEmailError: contact.email.$error,
225
hasPhoneError: contact.phone.$error,
226
hasAddressError: contact.address.$error
227
}
228
}
229
},
230
231
methods: {
232
// Validate specific sections
233
validatePersonalInfo() {
234
this.$v.user.personal.$touch()
235
return !this.$v.user.personal.$invalid
236
},
237
238
validateContact() {
239
this.$v.user.contact.$touch()
240
return !this.$v.user.contact.$invalid
241
}
242
}
243
}
244
```
245
246
### Group Validation
247
248
Reference-based validation for grouping related fields across different parts of the data structure.
249
250
```javascript { .api }
251
/**
252
* Group validation using array references
253
*/
254
interface GroupValidation {
255
[groupKey: string]: string[] | string;
256
}
257
```
258
259
**Usage:**
260
261
```javascript
262
export default {
263
data() {
264
return {
265
billingAddress: {
266
street: '',
267
city: '',
268
country: ''
269
},
270
shippingAddress: {
271
street: '',
272
city: '',
273
country: ''
274
},
275
sameAsShipping: false
276
}
277
},
278
279
validations: {
280
// Individual field validations
281
billingAddress: {
282
street: { required },
283
city: { required },
284
country: { required }
285
},
286
shippingAddress: {
287
street: { required },
288
city: { required },
289
country: { required }
290
},
291
292
// Group validation - validates related fields together
293
addressGroup: [
294
'billingAddress.street',
295
'billingAddress.city',
296
'billingAddress.country',
297
// Conditionally include shipping address
298
...(this.sameAsShipping ? [] : [
299
'shippingAddress.street',
300
'shippingAddress.city',
301
'shippingAddress.country'
302
])
303
]
304
},
305
306
computed: {
307
allAddressFieldsValid() {
308
return !this.$v.addressGroup.$invalid
309
}
310
},
311
312
watch: {
313
sameAsShipping(useShipping) {
314
if (useShipping) {
315
// Copy billing to shipping
316
Object.assign(this.shippingAddress, this.billingAddress)
317
}
318
// Re-evaluate validations due to dynamic group composition
319
this.$v.$touch()
320
}
321
}
322
}
323
```
324
325
### Dynamic Collections
326
327
Advanced patterns for dynamic validation with changing data structures.
328
329
**Usage:**
330
331
```javascript
332
export default {
333
data() {
334
return {
335
formFields: [
336
{ type: 'text', name: 'username', value: '', required: true },
337
{ type: 'email', name: 'email', value: '', required: true },
338
{ type: 'number', name: 'age', value: '', required: false }
339
]
340
}
341
},
342
343
// Dynamic validations based on field configuration
344
validations() {
345
const validations = {
346
formFields: {
347
$each: {
348
$trackBy: 'name',
349
value: {}
350
}
351
}
352
}
353
354
// Build validation rules based on field types
355
this.formFields.forEach((field, index) => {
356
const fieldValidations = {}
357
358
if (field.required) {
359
fieldValidations.required = required
360
}
361
362
switch (field.type) {
363
case 'email':
364
fieldValidations.email = email
365
break
366
case 'number':
367
fieldValidations.numeric = numeric
368
break
369
case 'text':
370
if (field.minLength) {
371
fieldValidations.minLength = minLength(field.minLength)
372
}
373
break
374
}
375
376
validations.formFields.$each.value = fieldValidations
377
})
378
379
return validations
380
},
381
382
computed: {
383
dynamicFormErrors() {
384
const errors = []
385
386
this.formFields.forEach((field, index) => {
387
const validation = this.$v.formFields.$iter[index]
388
if (validation && validation.value.$error) {
389
errors.push({
390
field: field.name,
391
errors: this.getFieldErrors(validation.value, field)
392
})
393
}
394
})
395
396
return errors
397
}
398
},
399
400
methods: {
401
addField(fieldConfig) {
402
this.formFields.push({
403
type: fieldConfig.type,
404
name: fieldConfig.name,
405
value: '',
406
required: fieldConfig.required || false
407
})
408
},
409
410
removeField(index) {
411
this.formFields.splice(index, 1)
412
},
413
414
getFieldErrors(validation, fieldConfig) {
415
const errors = []
416
417
if (fieldConfig.required && !validation.required) {
418
errors.push(`${fieldConfig.name} is required`)
419
}
420
421
if (fieldConfig.type === 'email' && !validation.email) {
422
errors.push(`${fieldConfig.name} must be a valid email`)
423
}
424
425
if (fieldConfig.type === 'number' && !validation.numeric) {
426
errors.push(`${fieldConfig.name} must be a number`)
427
}
428
429
return errors
430
}
431
}
432
}
433
```
434
435
### Complex Nested Arrays
436
437
Validation of arrays containing nested objects with their own collections.
438
439
**Usage:**
440
441
```javascript
442
export default {
443
data() {
444
return {
445
departments: [
446
{
447
name: 'Engineering',
448
manager: { name: 'John Doe', email: 'john@company.com' },
449
employees: [
450
{ name: 'Alice', position: 'Senior Developer' },
451
{ name: 'Bob', position: 'Junior Developer' }
452
]
453
}
454
]
455
}
456
},
457
458
validations: {
459
departments: {
460
$each: {
461
$trackBy: 'name',
462
name: { required, minLength: minLength(2) },
463
manager: {
464
name: { required },
465
email: { required, email }
466
},
467
employees: {
468
$each: {
469
$trackBy: 'name',
470
name: { required, minLength: minLength(2) },
471
position: { required }
472
}
473
}
474
}
475
}
476
},
477
478
computed: {
479
departmentSummary() {
480
return this.departments.map((dept, deptIndex) => {
481
const deptValidation = this.$v.departments.$iter[deptIndex]
482
483
return {
484
name: dept.name,
485
isValid: !deptValidation.$error,
486
managerValid: !deptValidation.manager.$error,
487
employeeCount: dept.employees.length,
488
validEmployees: dept.employees.filter((emp, empIndex) =>
489
!deptValidation.employees.$iter[empIndex].$error
490
).length
491
}
492
})
493
}
494
},
495
496
methods: {
497
addDepartment() {
498
this.departments.push({
499
name: '',
500
manager: { name: '', email: '' },
501
employees: []
502
})
503
},
504
505
addEmployee(deptIndex) {
506
this.departments[deptIndex].employees.push({
507
name: '',
508
position: ''
509
})
510
},
511
512
validateDepartment(index) {
513
const deptValidation = this.$v.departments.$iter[index]
514
deptValidation.$touch()
515
return !deptValidation.$invalid
516
}
517
}
518
}
519
```