Cara membuat form menggunakan validation

Create component UForm

UForm.vue
<template>
  <form @submit.prevent="onSubmit">
    <slot />
  </form>
</template>

<script lang="ts" setup>
import { provide, ref, onUnmounted, onMounted, computed } from 'vue'
import { useEventBus } from '@vueuse/core'

class FormException extends Error {
  constructor(message: string) {
    super(message)
    this.message = message
    Object.setPrototypeOf(this, FormException.prototype)
  }
}

const props = defineProps({
  schema: {
    type: Object,
    default: undefined,
  },
  state: {
    type: Object,
    required: true,
  },
  validate: {
    type: Function,
    default: () => [],
  },
  validateOn: {
    type: Array,
    default: () => ['blur', 'input', 'change', 'submit'],
  },
})

const emits = defineEmits(['submit', 'error'])

const bus = useEventBus('form-1')
const errors = ref<any[]>([])
const inputs = ref<Record<string, any>>({})

provide('form-errors', errors)
provide('form-events', bus)
provide('form-inputs', inputs)

onMounted(() => {
  bus.on(async (event: any) => {
    if (event.type !== 'submit' && props.validateOn?.includes(event.type)) {
      await performValidation(event.path, { silent: true })
    }
  })
})

onUnmounted(() => {
  bus.reset()
})

async function getErrors() {
  let errs = await props.validate(props.state)
  if (props.schema) {
    if (isZodSchema(props.schema)) {
      errs = errs.concat(await getZodErrors(props.state, props.schema))
    } else if (isYupSchema(props.schema)) {
      errs = errs.concat(await getYupErrors(props.state, props.schema))
    } else if (isJoiSchema(props.schema)) {
      errs = errs.concat(await getJoiErrors(props.state, props.schema))
    } else if (isValibotSchema(props.schema)) {
      errs = errs.concat(await getValibotError(props.state, props.schema))
    } else {
      throw new Error('Form validation failed: Unsupported form schema')
    }
  }
  return errs
}

async function performValidation(
  path?: string | string[],
  opts = { silent: false }
) {
  let paths = path
  if (path && !Array.isArray(path)) {
    paths = [path]
  }

  if (paths) {
    const otherErrors = errors.value.filter(
      (error) => !paths.includes(error.path)
    )
    const pathErrors = (await getErrors()).filter((error: { path: string }) =>
      paths.includes(error.path)
    )
    errors.value = otherErrors.concat(pathErrors)
  } else {
    errors.value = await getErrors()
  }

  if (errors.value.length > 0) {
    if (opts.silent) return false
    throw new FormException(
      `Form validation failed: ${JSON.stringify(errors.value, null, 2)}`
    )
  }

  return props.state
}

const localeData = computed(() => props.state)

async function onSubmit(payload: Event) {
  try {
    if (props.validateOn?.includes('submit')) {
      await performValidation()
    }
    console.log('Submitting form with state:', props.state)
    emits('submit', { ...payload, data: localeData })
  } catch (error) {
    if (error instanceof FormException) {
      emits('error', {
        ...payload,
        errors: errors.value.map((err) => ({
          ...err,
          id: inputs.value[err.path],
        })),
      })
    } else {
      // For other unexpected errors, log or rethrow
      console.error('Unexpected error during form submission:', error)
      throw error // Optional: Decide if the error should propagate
    }
  }
}

function isYupSchema(schema: any): boolean {
  return schema.validate && schema.__isYupSchema__
}

function isYupError(error: any): boolean {
  return error.inner !== undefined
}

async function getYupErrors(state: any, schema: any) {
  try {
    await schema.validate(state, { abortEarly: false })
    return []
  } catch (error: any) {
    if (isYupError(error)) {
      return error.inner.map((issue: any) => ({
        path: issue.path ?? '',
        message: issue.message,
      }))
    } else {
      throw error
    }
  }
}

function isZodSchema(schema: any): boolean {
  return schema.parse !== undefined
}

async function getZodErrors(state: any, schema: any) {
  const result = await schema.safeParseAsync(state)
  if (!result.success) {
    return result.error.issues.map((issue: any) => ({
      path: issue.path.join('.'),
      message: issue.message,
    }))
  }
  return []
}

function isJoiSchema(schema: any): boolean {
  return schema.validateAsync !== undefined && schema.id !== undefined
}

function isJoiError(error: any): boolean {
  return error.isJoi === true
}

async function getJoiErrors(state: any, schema: any) {
  try {
    await schema.validateAsync(state, { abortEarly: false })
    return []
  } catch (error: any) {
    if (isJoiError(error)) {
      return error.details.map((detail: any) => ({
        path: detail.path.join('.'),
        message: detail.message,
      }))
    } else {
      throw error
    }
  }
}

function isValibotSchema(schema: any): boolean {
  return schema._parse !== undefined
}

async function getValibotError(state: any, schema: any) {
  const result = await schema._parse(state)
  if (result.issues) {
    return result.issues.map((issue: any) => ({
      path: issue.path?.map((p: any) => p.key).join('.') || '',
      message: issue.message,
    }))
  }
  return []
}
</script>

create UFormGroup

UFormGroup.vue
<template>
  <div class="form-group">
    <label :for="name">{{ label }}</label>
    <slot
      :on-blur="handleBlur"
      :on-input="handleInput"
      :on-change="handleChange"
      :error="error"
    />
    <p v-if="error" class="text-red-500 text-sm mt-1">{{ error }}</p>
  </div>
</template>

<script lang="ts">
import { inject, computed } from 'vue'

export default {
  props: {
    label: {
      type: String,
      default: '',
    },
    name: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    // Inject form errors and events
    const errors = inject<{ value: { path: string; message: string }[] }>(
      'form-errors'
    )
    const formEvents = inject<{
      emit: (event: { type: string; path: string }) => Promise<void>
    }>('form-events')

    // Compute error for this specific field
    const error = computed(() => {
      if (!errors?.value) return ''
      const fieldError = errors.value.find((err) => err.path === props.name)
      return fieldError ? fieldError.message : ''
    })

    // Handle blur event
    const handleBlur = async () => {
      try {
        if (formEvents) {
          await formEvents.emit({ type: 'blur', path: props.name })
        }
      } catch (e) {
        console.error(e)
      }
    }

    // Handle input event to clear validation
    const handleInput = async () => {
      try {
        if (formEvents) {
          await formEvents.emit({ type: 'input', path: props.name })
        }
      } catch (e) {
        console.error(e)
      }
    }

    const handleChange = async () => {
      try {
        if (formEvents) {
          await formEvents.emit({ type: 'change', path: props.name })
        }
      } catch (e) {
        console.error(e)
      }
    }

    return {
      error,
      handleBlur,
      handleInput,
      handleChange,
    }
  },
}
</script>

create component

app.vue
<template>
  <UForm @submit="handleSubmit" :schema="ZOD_NOTES" :state="form">
    <UFormGroup name="name" label="Name">
      <template #default="{ onBlur, error }">
        <Input v-model="form.name" @blur="onBlur" :error="error"> </Input>
      </template>
    </UFormGroup>
  </UForm>
  <button
    type="submit"
    class="bg-blue-700 mt-2 w-full text-white p-2 rounded-lg"
  >
    Submit
  </button>
</template>

<script lang="ts" setup>
const form = ref({ name: '' })

const ZOD_NOTES = z.object({
  name: z.string().min(2).max(20),
})
const handleSubmit = async (event: { data: any }) => {
  console.log(event.data) // for get state when validation true
}
</script>

Contact us

Perumahan Griya Pratama Asri 2 No.H-12, Somoragen, Joho, Kec. Prambanan, Kabupaten Klaten, Jawa Tengah 57454
Find Me
iconiconiconiconhttps://www.buymeacoffee.com/outyourimpossible
2024-PRESENT © Muh Syahendra A