Skip to content

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

Released under the MIT License