or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

collections.mdcustom-validators.mdindex.mdintegration.mdvalidation-state.mdvalidators.md

collections.mddocs/

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

```