or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration-rules.mdcore-validation.mdfield-management.mdform-actions.mdform-management.mdindex.mdstate-access.mdvue-components.md

vue-components.mddocs/

0

# Vue Components

1

2

Renderless Vue components for template-based form development with integrated validation. These components provide declarative APIs for building forms while maintaining full control over rendering and styling.

3

4

## Capabilities

5

6

### Field Component

7

8

Renderless field component that provides validation and state management for individual form fields.

9

10

```typescript { .api }

11

/**

12

* Renderless field component for individual form fields

13

* Provides validation, state management, and binding objects via slot props

14

*/

15

interface FieldProps {

16

name: string; // Field path/name (required)

17

rules?: RuleExpression; // Validation rules

18

as?: string | Component; // Render as specific element/component

19

validateOnMount?: boolean; // Validate when field mounts

20

validateOnBlur?: boolean; // Validate on blur events

21

validateOnChange?: boolean; // Validate on change events

22

validateOnInput?: boolean; // Validate on input events

23

validateOnModelUpdate?: boolean; // Validate on v-model updates

24

bails?: boolean; // Stop validation on first error

25

label?: string; // Field label for error messages

26

uncheckedValue?: any; // Value when checkbox/radio unchecked

27

modelValue?: any; // v-model binding value

28

keepValue?: boolean; // Preserve value on unmount

29

}

30

31

interface FieldSlotProps {

32

field: FieldBindingObject; // Input binding object

33

componentField: ComponentFieldBindingObject; // Component binding object

34

value: any; // Current field value

35

meta: FieldMeta; // Field metadata

36

errors: string[]; // Field errors array

37

errorMessage: string | undefined; // First error message

38

validate: () => Promise<ValidationResult>; // Manual validation trigger

39

resetField: (state?: Partial<FieldState>) => void; // Reset field state

40

handleChange: (value: any) => void; // Handle value changes

41

handleBlur: () => void; // Handle blur events

42

setValue: (value: any) => void; // Set field value

43

setTouched: (touched: boolean) => void; // Set touched state

44

setErrors: (errors: string[]) => void; // Set field errors

45

}

46

47

interface FieldBindingObject {

48

name: string;

49

onBlur: () => void;

50

onChange: (e: Event) => void;

51

onInput: (e: Event) => void;

52

value: any;

53

}

54

55

interface ComponentFieldBindingObject {

56

modelValue: any;

57

'onUpdate:modelValue': (value: any) => void;

58

onBlur: () => void;

59

}

60

```

61

62

**Field Component Examples:**

63

64

```vue

65

<template>

66

<!-- Basic field with custom input -->

67

<Field name="email" :rules="emailRules" v-slot="{ field, errorMessage, meta }">

68

<input

69

v-bind="field"

70

type="email"

71

placeholder="Enter your email"

72

:class="{

73

'error': !meta.valid && meta.touched,

74

'success': meta.valid && meta.touched

75

}"

76

/>

77

<span v-if="errorMessage" class="error-message">{{ errorMessage }}</span>

78

</Field>

79

80

<!-- Field rendered as specific element -->

81

<Field name="message" as="textarea" rules="required" v-slot="{ field, errorMessage }">

82

<label>Message</label>

83

<!-- Field is rendered as textarea automatically -->

84

<span v-if="errorMessage">{{ errorMessage }}</span>

85

</Field>

86

87

<!-- Field with component binding -->

88

<Field name="category" rules="required" v-slot="{ componentField, errorMessage }">

89

<CustomSelect v-bind="componentField" :options="categories" />

90

<ErrorMessage name="category" />

91

</Field>

92

93

<!-- Checkbox field -->

94

<Field

95

name="newsletter"

96

type="checkbox"

97

:unchecked-value="false"

98

:value="true"

99

v-slot="{ field, value }"

100

>

101

<label>

102

<input v-bind="field" type="checkbox" />

103

Subscribe to newsletter ({{ value ? 'Yes' : 'No' }})

104

</label>

105

</Field>

106

107

<!-- Field with custom validation -->

108

<Field

109

name="username"

110

:rules="validateUsername"

111

v-slot="{ field, errorMessage, meta, validate }"

112

>

113

<input v-bind="field" placeholder="Username" />

114

<button @click="validate" :disabled="meta.pending">

115

{{ meta.pending ? 'Validating...' : 'Check Availability' }}

116

</button>

117

<span v-if="errorMessage">{{ errorMessage }}</span>

118

</Field>

119

120

<!-- Field with advanced state management -->

121

<Field

122

name="password"

123

rules="required|min:8"

124

v-slot="{ field, meta, errors, setValue, setTouched }"

125

>

126

<input

127

v-bind="field"

128

type="password"

129

placeholder="Password"

130

@focus="setTouched(true)"

131

/>

132

133

<!-- Password strength indicator -->

134

<div class="password-strength">

135

<div

136

v-for="error in errors"

137

:key="error"

138

class="strength-rule"

139

:class="{ 'met': !errors.includes(error) }"

140

>

141

{{ error }}

142

</div>

143

</div>

144

145

<button @click="setValue(generatePassword())">

146

Generate Strong Password

147

</button>

148

</Field>

149

</template>

150

151

<script setup lang="ts">

152

import { Field, ErrorMessage } from 'vee-validate';

153

154

const emailRules = (value: string) => {

155

if (!value) return 'Email is required';

156

if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Email is invalid';

157

return true;

158

};

159

160

const validateUsername = async (value: string) => {

161

if (!value) return 'Username is required';

162

if (value.length < 3) return 'Username too short';

163

164

// Async validation

165

const response = await fetch(`/api/check-username?username=${value}`);

166

const { available } = await response.json();

167

168

return available || 'Username is taken';

169

};

170

171

const categories = [

172

{ value: 'tech', label: 'Technology' },

173

{ value: 'design', label: 'Design' },

174

{ value: 'business', label: 'Business' }

175

];

176

177

const generatePassword = () => {

178

return Math.random().toString(36).slice(-12) + 'A1!';

179

};

180

</script>

181

```

182

183

### Form Component

184

185

Form wrapper component that provides validation context and handles form submission.

186

187

```typescript { .api }

188

/**

189

* Form wrapper component with validation context

190

* Provides form state management and submission handling via slot props

191

*/

192

interface FormProps {

193

as?: string | Component; // Render as specific element (default: 'form')

194

validationSchema?: object; // Form validation schema

195

initialValues?: object; // Initial field values

196

initialErrors?: object; // Initial field errors

197

initialTouched?: object; // Initial field touched states

198

validateOnMount?: boolean; // Validate when form mounts

199

onSubmit?: SubmissionHandler; // Form submission handler

200

onInvalidSubmit?: InvalidSubmissionHandler; // Invalid submission handler

201

keepValues?: boolean; // Preserve values on unmount

202

name?: string; // Form identifier

203

}

204

205

interface FormSlotProps {

206

// Form state

207

values: Record<string, any>; // Current form values

208

errors: Record<string, string>; // Current form errors

209

meta: FormMeta; // Form metadata

210

isSubmitting: boolean; // Submission state

211

isValidating: boolean; // Validation state

212

submitCount: number; // Submission attempt count

213

214

// Form methods

215

handleSubmit: (e?: Event) => Promise<void>; // Form submission handler

216

handleReset: () => void; // Form reset handler

217

validate: () => Promise<FormValidationResult>; // Manual form validation

218

validateField: (field: string) => Promise<ValidationResult>; // Manual field validation

219

220

// State mutations

221

setFieldValue: (field: string, value: any) => void; // Set field value

222

setFieldError: (field: string, error: string) => void; // Set field error

223

setErrors: (errors: Record<string, string>) => void; // Set multiple errors

224

setValues: (values: Record<string, any>) => void; // Set multiple values

225

setTouched: (touched: Record<string, boolean>) => void; // Set touched states

226

resetForm: (state?: Partial<FormState>) => void; // Reset form

227

resetField: (field: string, state?: Partial<FieldState>) => void; // Reset field

228

}

229

```

230

231

**Form Component Examples:**

232

233

```vue

234

<template>

235

<!-- Basic form with validation schema -->

236

<Form

237

:validationSchema="schema"

238

:initial-values="initialValues"

239

@submit="onSubmit"

240

v-slot="{ errors, meta, isSubmitting }"

241

>

242

<Field name="name" v-slot="{ field, errorMessage }">

243

<input v-bind="field" placeholder="Full Name" />

244

<span v-if="errorMessage">{{ errorMessage }}</span>

245

</Field>

246

247

<Field name="email" v-slot="{ field, errorMessage }">

248

<input v-bind="field" type="email" placeholder="Email" />

249

<span v-if="errorMessage">{{ errorMessage }}</span>

250

</Field>

251

252

<button

253

type="submit"

254

:disabled="!meta.valid || isSubmitting"

255

>

256

{{ isSubmitting ? 'Submitting...' : 'Submit' }}

257

</button>

258

259

<!-- Form-level error display -->

260

<div v-if="Object.keys(errors).length > 0" class="form-errors">

261

<h4>Please fix the following errors:</h4>

262

<ul>

263

<li v-for="(error, field) in errors" :key="field">

264

{{ field }}: {{ error }}

265

</li>

266

</ul>

267

</div>

268

</Form>

269

270

<!-- Advanced form with manual submission -->

271

<Form

272

:validation-schema="userSchema"

273

v-slot="{

274

values,

275

errors,

276

meta,

277

handleSubmit,

278

setFieldValue,

279

setErrors,

280

resetForm,

281

isSubmitting

282

}"

283

>

284

<Field name="username" v-slot="{ field, errorMessage }">

285

<input v-bind="field" placeholder="Username" />

286

<span v-if="errorMessage">{{ errorMessage }}</span>

287

</Field>

288

289

<Field name="email" v-slot="{ field, errorMessage }">

290

<input v-bind="field" type="email" placeholder="Email" />

291

<span v-if="errorMessage">{{ errorMessage }}</span>

292

</Field>

293

294

<!-- Manual submission buttons -->

295

<button @click="handleSubmit(submitUser)" :disabled="!meta.valid">

296

Create User

297

</button>

298

299

<button @click="handleSubmit(saveDraft)" type="button">

300

Save as Draft

301

</button>

302

303

<button @click="resetForm" type="button">

304

Reset Form

305

</button>

306

307

<!-- Programmatic field updates -->

308

<button @click="setFieldValue('username', generateUsername())">

309

Generate Username

310

</button>

311

312

<!-- Form progress -->

313

<div class="form-progress">

314

Progress: {{ Math.round((Object.keys(values).filter(key => values[key]).length / Object.keys(schema).length) * 100) }}%

315

</div>

316

</Form>

317

318

<!-- Form with custom validation handling -->

319

<Form

320

@submit="handleFormSubmit"

321

@invalid-submit="handleInvalidSubmit"

322

v-slot="{ meta, submitCount }"

323

>

324

<Field name="data" rules="required" v-slot="{ field, errorMessage }">

325

<input v-bind="field" placeholder="Enter data" />

326

<span v-if="errorMessage">{{ errorMessage }}</span>

327

</Field>

328

329

<button type="submit">Submit</button>

330

331

<div v-if="submitCount > 0">

332

Attempts: {{ submitCount }}

333

</div>

334

</Form>

335

</template>

336

337

<script setup lang="ts">

338

import { Form, Field } from 'vee-validate';

339

import * as yup from 'yup';

340

341

const schema = yup.object({

342

name: yup.string().required('Name is required'),

343

email: yup.string().email('Invalid email').required('Email is required')

344

});

345

346

const userSchema = yup.object({

347

username: yup.string().min(3).required(),

348

email: yup.string().email().required()

349

});

350

351

const initialValues = {

352

name: '',

353

email: ''

354

};

355

356

const onSubmit = async (values: any) => {

357

console.log('Form submitted:', values);

358

359

// Simulate API call

360

await new Promise(resolve => setTimeout(resolve, 1000));

361

362

alert('Form submitted successfully!');

363

};

364

365

const submitUser = async (values: any, { setErrors }: any) => {

366

try {

367

const response = await fetch('/api/users', {

368

method: 'POST',

369

body: JSON.stringify(values)

370

});

371

372

if (!response.ok) {

373

const errors = await response.json();

374

setErrors(errors);

375

return;

376

}

377

378

alert('User created successfully!');

379

} catch (error) {

380

setErrors({ '': 'Network error occurred' });

381

}

382

};

383

384

const saveDraft = async (values: any) => {

385

await fetch('/api/drafts', {

386

method: 'POST',

387

body: JSON.stringify(values)

388

});

389

390

alert('Draft saved!');

391

};

392

393

const generateUsername = () => {

394

return 'user_' + Math.random().toString(36).substr(2, 9);

395

};

396

397

const handleFormSubmit = (values: any) => {

398

console.log('Valid form submitted:', values);

399

};

400

401

const handleInvalidSubmit = ({ errors, values }: any) => {

402

console.log('Invalid form submission:', { errors, values });

403

alert('Please fix form errors before submitting');

404

};

405

</script>

406

```

407

408

### FieldArray Component

409

410

Component for managing dynamic arrays of form fields with built-in manipulation methods.

411

412

```typescript { .api }

413

/**

414

* Dynamic array field management component

415

* Provides array manipulation methods via slot props

416

*/

417

interface FieldArrayProps {

418

name: string; // Array field path (required)

419

}

420

421

interface FieldArraySlotProps {

422

fields: FieldEntry[]; // Array of field entries

423

push: (value: any) => void; // Add item to end

424

remove: (index: number) => void; // Remove item by index

425

swap: (indexA: number, indexB: number) => void; // Swap two items

426

insert: (index: number, value: any) => void; // Insert item at index

427

replace: (newArray: any[]) => void; // Replace entire array

428

update: (index: number, value: any) => void; // Update item at index

429

prepend: (value: any) => void; // Add item to beginning

430

move: (oldIndex: number, newIndex: number) => void; // Move item to new position

431

}

432

433

interface FieldEntry {

434

value: any; // Entry value

435

key: string | number; // Unique key for tracking

436

isFirst: boolean; // True if first entry

437

isLast: boolean; // True if last entry

438

}

439

```

440

441

**FieldArray Component Examples:**

442

443

```vue

444

<template>

445

<!-- Simple array of strings -->

446

<FieldArray name="tags" v-slot="{ fields, push, remove }">

447

<div v-for="(entry, index) in fields" :key="entry.key" class="tag-item">

448

<Field :name="`tags[${index}]`" v-slot="{ field, errorMessage }">

449

<input v-bind="field" placeholder="Enter tag" />

450

<span v-if="errorMessage">{{ errorMessage }}</span>

451

</Field>

452

453

<button @click="remove(index)" type="button">Remove</button>

454

</div>

455

456

<button @click="push('')" type="button">Add Tag</button>

457

</FieldArray>

458

459

<!-- Complex array of objects -->

460

<FieldArray name="users" v-slot="{ fields, push, remove, swap, move }">

461

<div v-for="(entry, index) in fields" :key="entry.key" class="user-item">

462

<div class="user-fields">

463

<Field :name="`users[${index}].name`" v-slot="{ field, errorMessage }">

464

<input v-bind="field" placeholder="Name" />

465

<span v-if="errorMessage">{{ errorMessage }}</span>

466

</Field>

467

468

<Field :name="`users[${index}].email`" v-slot="{ field, errorMessage }">

469

<input v-bind="field" type="email" placeholder="Email" />

470

<span v-if="errorMessage">{{ errorMessage }}</span>

471

</Field>

472

473

<Field :name="`users[${index}].role`" v-slot="{ field }">

474

<select v-bind="field">

475

<option value="">Select Role</option>

476

<option value="admin">Admin</option>

477

<option value="user">User</option>

478

<option value="guest">Guest</option>

479

</select>

480

</Field>

481

</div>

482

483

<div class="user-actions">

484

<button @click="remove(index)" type="button">Remove</button>

485

486

<button

487

@click="move(index, index - 1)"

488

:disabled="entry.isFirst"

489

type="button"

490

>

491

Move Up

492

</button>

493

494

<button

495

@click="move(index, index + 1)"

496

:disabled="entry.isLast"

497

type="button"

498

>

499

Move Down

500

</button>

501

</div>

502

</div>

503

504

<button @click="push(defaultUser)" type="button">Add User</button>

505

506

<button @click="push(defaultUser, 0)" type="button">Add User at Top</button>

507

</FieldArray>

508

509

<!-- Advanced field array with drag and drop -->

510

<FieldArray

511

name="sortableItems"

512

v-slot="{ fields, remove, swap, update }"

513

>

514

<draggable

515

v-model="fields"

516

@end="handleDragEnd"

517

item-key="key"

518

>

519

<template #item="{ element: entry, index }">

520

<div class="sortable-item">

521

<div class="drag-handle">⋮⋮</div>

522

523

<Field :name="`sortableItems[${index}].title`" v-slot="{ field }">

524

<input v-bind="field" placeholder="Item title" />

525

</Field>

526

527

<Field :name="`sortableItems[${index}].description`" v-slot="{ field }">

528

<textarea v-bind="field" placeholder="Description"></textarea>

529

</Field>

530

531

<button @click="remove(index)" type="button">×</button>

532

</div>

533

</template>

534

</draggable>

535

</FieldArray>

536

</template>

537

538

<script setup lang="ts">

539

import { FieldArray, Field } from 'vee-validate';

540

import draggable from 'vuedraggable';

541

542

const defaultUser = {

543

name: '',

544

email: '',

545

role: ''

546

};

547

548

const handleDragEnd = (event: any) => {

549

// Draggable automatically updates the fields array

550

console.log('Items reordered:', event);

551

};

552

</script>

553

```

554

555

### ErrorMessage Component

556

557

Conditional error message display component that only renders when a field has an error.

558

559

```typescript { .api }

560

/**

561

* Conditional error message display component

562

* Only renders when specified field has an error message

563

*/

564

interface ErrorMessageProps {

565

name: string; // Field path to show error for (required)

566

as?: string; // Render as specific element

567

}

568

569

interface ErrorMessageSlotProps {

570

message: string | undefined; // Error message or undefined

571

}

572

```

573

574

**ErrorMessage Component Examples:**

575

576

```vue

577

<template>

578

<!-- Basic error message display -->

579

<Field name="email" rules="required|email" v-slot="{ field }">

580

<input v-bind="field" type="email" placeholder="Email" />

581

</Field>

582

<ErrorMessage name="email" />

583

584

<!-- Custom error message styling -->

585

<Field name="password" rules="required|min:8" v-slot="{ field }">

586

<input v-bind="field" type="password" placeholder="Password" />

587

</Field>

588

<ErrorMessage name="password" as="div" class="error-text" />

589

590

<!-- Error message with custom slot -->

591

<Field name="username" rules="required" v-slot="{ field }">

592

<input v-bind="field" placeholder="Username" />

593

</Field>

594

<ErrorMessage name="username" v-slot="{ message }">

595

<div v-if="message" class="error-container">

596

<icon name="warning" />

597

<span>{{ message }}</span>

598

</div>

599

</ErrorMessage>

600

601

<!-- Multiple error messages for different fields -->

602

<div class="form-group">

603

<Field name="firstName" rules="required" v-slot="{ field }">

604

<input v-bind="field" placeholder="First Name" />

605

</Field>

606

<ErrorMessage name="firstName" />

607

</div>

608

609

<div class="form-group">

610

<Field name="lastName" rules="required" v-slot="{ field }">

611

<input v-bind="field" placeholder="Last Name" />

612

</Field>

613

<ErrorMessage name="lastName" />

614

</div>

615

616

<!-- Error message for nested fields -->

617

<Field name="address.street" rules="required" v-slot="{ field }">

618

<input v-bind="field" placeholder="Street Address" />

619

</Field>

620

<ErrorMessage name="address.street" />

621

622

<Field name="address.city" rules="required" v-slot="{ field }">

623

<input v-bind="field" placeholder="City" />

624

</Field>

625

<ErrorMessage name="address.city" />

626

627

<!-- Conditional error message display -->

628

<Field name="phone" :rules="phoneRules" v-slot="{ field, meta }">

629

<input v-bind="field" placeholder="Phone Number" />

630

</Field>

631

<ErrorMessage

632

name="phone"

633

v-slot="{ message }"

634

>

635

<div v-if="message && showPhoneError" class="phone-error">

636

{{ message }}

637

<button @click="showPhoneError = false">Dismiss</button>

638

</div>

639

</ErrorMessage>

640

</template>

641

642

<script setup lang="ts">

643

import { Field, ErrorMessage } from 'vee-validate';

644

import { ref } from 'vue';

645

646

const showPhoneError = ref(true);

647

648

const phoneRules = (value: string) => {

649

if (!value) return 'Phone number is required';

650

if (!/^\d{10}$/.test(value)) return 'Phone number must be 10 digits';

651

return true;

652

};

653

</script>

654

655

<style scoped>

656

.error-text {

657

color: red;

658

font-size: 0.875rem;

659

margin-top: 0.25rem;

660

}

661

662

.error-container {

663

display: flex;

664

align-items: center;

665

gap: 0.5rem;

666

color: red;

667

font-size: 0.875rem;

668

}

669

670

.form-group {

671

margin-bottom: 1rem;

672

}

673

674

.phone-error {

675

background: #fee;

676

border: 1px solid #fcc;

677

padding: 0.5rem;

678

border-radius: 4px;

679

color: #c00;

680

}

681

</style>

682

```

683

684

## Component Integration Patterns

685

686

### Form with Mixed Field Types

687

688

Combining different field types in a comprehensive form.

689

690

```vue

691

<template>

692

<Form

693

:validation-schema="schema"

694

:initial-values="initialValues"

695

@submit="onSubmit"

696

v-slot="{ meta, isSubmitting }"

697

>

698

<!-- Text input -->

699

<Field name="name" v-slot="{ field, errorMessage }">

700

<label>Full Name</label>

701

<input v-bind="field" type="text" />

702

<ErrorMessage name="name" />

703

</Field>

704

705

<!-- Email input with custom validation -->

706

<Field name="email" v-slot="{ field, errorMessage, meta }">

707

<label>Email Address</label>

708

<input

709

v-bind="field"

710

type="email"

711

:class="{ valid: meta.valid && meta.touched }"

712

/>

713

<ErrorMessage name="email" />

714

</Field>

715

716

<!-- Select dropdown -->

717

<Field name="country" v-slot="{ field }">

718

<label>Country</label>

719

<select v-bind="field">

720

<option value="">Select Country</option>

721

<option value="us">United States</option>

722

<option value="ca">Canada</option>

723

<option value="uk">United Kingdom</option>

724

</select>

725

<ErrorMessage name="country" />

726

</Field>

727

728

<!-- Checkbox -->

729

<Field

730

name="agreeToTerms"

731

type="checkbox"

732

:value="true"

733

v-slot="{ field }"

734

>

735

<label>

736

<input v-bind="field" type="checkbox" />

737

I agree to the terms and conditions

738

</label>

739

<ErrorMessage name="agreeToTerms" />

740

</Field>

741

742

<!-- Dynamic field array -->

743

<FieldArray name="hobbies" v-slot="{ fields, push, remove }">

744

<label>Hobbies</label>

745

<div v-for="(entry, index) in fields" :key="entry.key">

746

<Field :name="`hobbies[${index}]`" v-slot="{ field }">

747

<input v-bind="field" placeholder="Enter hobby" />

748

</Field>

749

<button @click="remove(index)" type="button">Remove</button>

750

</div>

751

<button @click="push('')" type="button">Add Hobby</button>

752

</FieldArray>

753

754

<!-- Submit button -->

755

<button

756

type="submit"

757

:disabled="!meta.valid || isSubmitting"

758

>

759

{{ isSubmitting ? 'Submitting...' : 'Submit' }}

760

</button>

761

</Form>

762

</template>

763

```