Form Using Field Components
This example is using the PrimeVue preset
Source code
vue
<template>
<Enforma
ref="formRef"
:data="data"
:validator="validator"
:submit-handler="submitHandler"
>
<div class="grid grid-cols-2 gap-4">
<EnformaField
class="col-start-1 col-end-3"
name="name"
required
label="Name"
:input-props="{class: 'w-full'}"
/>
<!--
validation rules and custom error messages
can be passed at field level as well using the
`rules` and `messages` props like below
-->
<EnformaField
class="col-start-1 col-end-3"
name="email"
required
label="Email"
:input-props="{class: 'w-full'}"
/>
<EnformaField
class="col-start-1 col-end-2"
name="address.country"
required
label="Country"
:input-props="{class: 'w-full'}"
/>
<EnformaField
class="col-start-2 col-end-3"
name="address.city"
required
label="City"
:input-props="{class: 'w-full'}"
/>
<EnformaField
class="col-start-1 col-end-3 toggle-field"
name="willing_to_relocate"
label="Willing to relocate"
showLabelNextToInput
input-component="toggle"
/>
<EnformaField
class="col-start-1 col-end-2"
name="salary"
label="Salary"
:input-component="SalaryField"
:input-props="{class: 'w-full'}"
/>
<EnformaField
class="col-start-2 col-end-3"
name="available_date"
label="Available date"
inputComponent="datepicker"
useModelValue
:input-props="{class: 'w-full', dateFormat: 'yy-mm-dd', fluid: true}"
/>
<EnformaField
name="linkedin_profile"
label="Linkedin Profile"
:input-props="{class: 'w-full'}"
/>
<EnformaField
name="personal_site"
label="Personal site"
:input-props="{class: 'w-full'}"
/>
</div>
<h3 class="w-full">Skills</h3>
<EnformaRepeatableTable
class="mb-4"
name="skills"
:subfields="skillFields"
/>
<h3 class="w-full">Experience</h3>
<div>
<EnformaRepeatable
class="mb-4 form-repeatable-experience"
name="experience"
:subfields="experienceFields"
/>
</div>
</Enforma>
<h5 class="mt-8 mb-4">Manipulating the form from outside</h5>
<div class="flex gap-2">
<Button
severity="secondary"
@click="formRef?.submit()"
label="Submit"
:loading="formRef?.$isSubmitting" />
<Button
severity="secondary"
@click="formRef?.setFieldValue('name', 'John Doe')"
label="Set name to 'John Doe'" />
<Button
severity="secondary"
@click="formRef?.add('skills', 0, {name: 'new skill', level: 'Expert'})"
label="Prepend new skill" />
</div>
<h5 class="mt-8 mb-4">Accessing the form details from outside</h5>
<strong>First skill</strong>: <code>{{ formRef?.getFieldValue('skills.0.name') }}</code><br>
<strong>Email errors</strong>: <code>{{ formRef?.getFieldErrors('email') }}</code><br>
</template>
<script setup>
import { Enforma, EnformaField, EnformaRepeatable, EnformaRepeatableTable } from '@'
import useFormConfig from '../headless/useFormConfig'
import EndDateField from './EndDateField.vue'
import SalaryField from './SalaryField.vue'
import { Button } from 'primevue'
import { ref } from 'vue'
// for accessing the FormController
const formRef = ref()
const {data, validator, submitHandler} = useFormConfig()
const skillFields = {
name: {
label: "Skill",
inputComponent: 'input', // not necessary with the Primevue preset, it's the default
inputProps: {
fluid: true
}
},
level: {
label: "Level",
inputComponent: 'select',
inputProps: {
fluid: true,
options: ['Beginner', 'Intermediate', 'Advanced', 'Expert']
}
}
}
const experienceFields = {
company: {
label: "Company",
wrapperProps: {
class: 'col-start-1 col-end-2'
},
inputProps: {
fluid: true
},
},
position: {
label: "Position",
wrapperProps: {
class: 'col-start-2 col-end-3'
},
inputProps: {
fluid: true
},
},
start: {
label: "Start",
useModelValue: true,
inputComponent: 'datepicker',
inputProps: {
dateFormat: "yy-mm-dd",
fluid: true
},
},
end: {
label: "End",
useModelValue: true,
inputComponent: EndDateField,
inputProps: {
fluid: true
},
},
}
</script>vue
<!--
This custom field that is a wrapper for
a <HeadlessField> component that renders 2 input fields
It doesn't render the errors, or label, it just renders the inputs
-->
<template>
<HeadlessField :name="endName">
<template #default="end">
<DatePicker
:id="end.id"
:model-value="end.value"
date-format="yy-mm-dd"
fluid
:disabled="isCurrentlyWorking"
v-bind="end.attrs"
@update:modelValue="end.events['update:modelValue']"
/>
<HeadlessField :name="currentName">
<template #default="current">
<div class="flex align-center mt-2">
<ToggleSwitch
:id="current.id"
class="me-2"
:model-value="current.value"
v-bind="current.attrs"
:true-value="true"
:false-value="false"
@change="(evt) => onChangeCurrent(evt.srcElement?.checked)"
/>
<span @click="onChangeCurrent(!current.value)">Currently working here</span>
</div>
</template>
</HeadlessField>
</template>
</HeadlessField>
</template>
<script setup>
import { formControllerKey, HeadlessField } from '@'
import { DatePicker, ToggleSwitch } from 'primevue'
import { inject, computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
}
})
const endName = props.name
const currentName = endName.replace('.end', '.current')
const form = inject(formControllerKey)
const isCurrentlyWorking = computed(() => form[currentName])
const onChangeCurrent = (value) => {
debugger
form.setFieldValue(currentName, value)
form.setFieldValue(endName, null)
form.validateField(endName, true)
}
</script>vue
<template>
<HeadlessField :name="minName">
<template #default="min">
<HeadlessField :name="maxName">
<template #default="max">
<div class="flex gap-1 items-center">
<InputText
:id="min.id"
style="width: 100px"
v-bind="min.attrs"
v-on="min.events"
placeholder="Min"
/>
<span>-</span>
<InputText
:id="max.id"
style="width: 100px"
v-bind="max.attrs"
v-on="max.events"
placeholder="Max"
/>
</div>
<div v-if="min.error"
:id="min.attrs['aria-errormessage']"
class="text-red-500">
{{ min.error }}
</div>
<div v-else-if="max.error"
:id="max.attrs['aria-errormessage']"
class="text-red-500">
{{ max.error }}
</div>
</template>
</HeadlessField>
</template>
</HeadlessField>
</template>
<script setup>
import { HeadlessField } from '@'
import { InputText } from 'primevue'
import { useAttrs } from 'vue'
const attrs = useAttrs()
const props = defineProps({
name: {
type: String,
required: true
}
})
const minName = props.name + '.min'
const maxName = props.name + '.max'
</script>ts
import { createEncolaValidator } from '../../../src/validators/encolaValidator'
// this file contains a composable function to be reused for the examples
function getData() {
const data = {
name: "",
email: '',
address: {
country: '',
city: '',
},
available_date: null,
willing_to_relocate: false,
skills: [],
experience: [],
}
for (let i = 0; i < 3; i++) {
const level = ['Beginner', 'Intermediate', 'Advanced', 'Expert'].sort(() => Math.random() - 0.5)[0]
data.skills.push({ name: `Skill ${i + 1}`, level })
}
for (let i = 0; i < 2; i++) {
const level = ['Beginner', 'Intermediate', 'Advanced', 'Expert'].sort(() => Math.random() - 0.5)[0]
data.experience.push({
company: `Company ${i + 1}`,
position: `Job title ${i + 1}`,
start: new Date(`202${5 - i}-01-01`),
end: i === 0 ? null : new Date(`202${5 - 1}-12-31`),
current: i === 0
})
}
return data
}
// this validator is based on Encola Validator
// but it can be replaced with a validator that uses Zod, Valibot or Yup
const validator = createEncolaValidator(
{
name: 'required',
email: 'required|email',
'salary.min': 'number',
'salary.max': 'number|gt:@salary.min',
'available_date': 'required|date:yy-mm-dd|date_after:' + (new Date().toISOString().split('T')[0]),
'address.city': 'required',
'address.country': 'required',
'linkedin_profile': 'required|url',
'personal_site': 'url',
'skills.*.name': 'required',
'skills.*.level': 'required',
'experience.*.company': 'required',
'experience.*.position': 'required',
'experience.*.start': 'required|date:yy-mm-dd',
'experience.*.end': 'required_when:@experience.*.current,false|date:yy-mm-dd',
},
{
'name:required': 'You gotta have a name',
'salary.max:gt': 'The max should be greater than the min'
}
)
// For backwards compatibility, also export rules and messages separately
const rules = {
name: 'required',
email: {
'required|email': true
}
}
const messages = {
email: {
'required': 'Email is required',
'email': 'Please enter a valid email'
}
}
const submitHandler = (formData) => {
return new Promise((resolve) => {
setTimeout(() => {
alert('Data sent to server: ' + JSON.stringify(formData))
resolve(true)
}, 2000)
})
}
export default function () {
return {
data: getData(),
validator,
submitHandler
}
}