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
```