logaretm / vee-validate

✅ Painless Vue forms
https://vee-validate.logaretm.com/v4
MIT License
10.65k stars 1.24k forks source link

Sample implementation for TW-Elements with Yup #4663

Closed noelc10 closed 4 months ago

noelc10 commented 4 months ago

What happened?

HI,

I just exploring on using this package with TW-Elements but it seems like there's no sample in the docs. Although the in-house validation on TW-Elements with their forms and other form elements is good but I need a realtime approach like in the previous versions where you can just wrap the form field element with validation-provider tag

I have a registration form using Nuxt 3, vee-validate and Yup where everytime a user interact with the form fields, it should be automatically validate the focused fields, and if the form fields is valid, that's the time the register button will be activated, hence, it should fill all the fields that can passed by the validation.

I have an idea on re-initiate the value of data-te-validated form attribute everytime from false to true when the user interact with the form but it seems like a bad logic, since I'm relying on the said form attribute.

Any workaround based on my sample code is much appreciated. And I hope the docs with update to provide at least a simple implementation on form validation via TW-Elements.

Thank you so much.

<script setup>
import { onMounted } from 'vue'
import { useForm } from 'vee-validate'
import { isEmpty } from 'lodash'
import * as yup from 'yup'

const emit = defineEmits(['register', 'clearRegisterError'])
const props = defineProps({
  formErrors: {
    type: Object,
    default: null
  }
})
const { meta, errors, setErrors, isSubmitting, isValidating, handleSubmit, defineField } = useForm({
  validationSchema: yup.object({
    name: yup.string().required('Name field is required'),
    email: yup.string().email('Email Address must be email').required('Email Address is required'),
    password: yup.string().min(6, 'Password should be longer than 6 characters').required('Password field is required'),
    password_confirmation: yup.string()
      .required('Confirm Password field is required')
      .oneOf([yup.ref('password')], 'Passwords do not match'),
  })
})
const [name, nameProps] = defineField('name');
const [email, emailProps] = defineField('email');
const [password, passwordProps] = defineField('password');
const [password_confirmation, passwordConfirmationProps] = defineField('password_confirmation');
const onSubmit = handleSubmit(values => {
  emit('register', values)
})
const submitDisabled = computed(() => isSubmitting.value || isValidating.value || !meta.value.valid || !!props.value?.formErrors)

function handleClearError (field) {
  emit('clearRegisterError', field)
}

setErrors({
  name: props.value?.formErrors?.name,
  email: props.value?.formErrors?.email,
  password: props.value?.formErrors?.password,
  password_confirmation: props.value?.formErrors?.password_confirmation,
})

onMounted(async () => {
  const {
    Input,
    Ripple,
    Validation,
    initTE,
  } = await import('tw-elements')

  initTE({ Input, Ripple, Validation }, { allowReinits: true })
})
</script>

<template>
  <form
    data-te-validation-init
    data-te-class-notch-leading-valid="border-[#e5e5e5] dark:border-[#e5e5e5] group-data-[te-input-focused]:shadow-[-1px_0_0_#3B71CA,_0_1px_0_0_#3B71CA,_0_-1px_0_0_#3B71CA] group-data-[te-input-focused]:border-[#3B71CA] !text-[#e5e5e5] !dark:text-primary placeholder:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0 peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
    data-te-class-notch-middle-valid="border-[#e5e5e5] dark:border-[#e5e5e5] group-data-[te-input-focused]:shadow-[0_1px_0_0_#3B71CA] group-data-[te-input-focused]:border-[#3B71CA] !text-[#e5e5e5] !dark:text-primary placeholder:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0 peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
    data-te-class-notch-trailing-valid="border-[#e5e5e5] dark:border-[#e5e5e5] group-data-[te-input-focused]:shadow-[1px_0_0_#3B71CA,_0_-1px_0_0_#3B71CA,_0_1px_0_0_#3B71CA] group-data-[te-input-focused]:border-[#3B71CA] !text-[#e5e5e5] !dark:text-primary placeholder:text-neutral-200 placeholder:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0 peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
    data-te-class-label-valid="!data-[te-input-state-active]:text-[#e5e5e5] !peer-focus-data-[te-input-state-active]:text-primary !peer-data-[te-input-state-active]:text-[#e5e5e5] !peer-focus-data-[te-input-state-active]:text-primary"
    data-te-class-invalid-feedback="absolute top-full left-0 m-1 w-auto text-sm text-[#dc4c64] animate-[fade-in_0.3s_both] capitalize-first-letter"
    data-te-valid-feedback="&nbsp;"
    data-te-active-validation="true"
    :data-te-validated="true"
    @submit.prevent="onSubmit"
  >
    <div
      class="relative mb-8"
      data-te-input-wrapper-init
      data-te-validate="input"
      :data-te-invalid-feedback="errors?.name || props.value?.formErrors?.name"
      :data-te-validation-state="errors?.name || props.value?.formErrors?.name ? 'invalid' : 'valid'"
    >
      <input
        v-model="name"
        v-bind="nameProps"
        type="text"
        class="peer block min-h-[auto] w-full rounded border-0 bg-transparent px-3 py-[0.32rem] leading-[2.15] outline-none transition-all duration-200 ease-linear focus:placeholder:opacity-100 data-[te-input-state-active]:placeholder:opacity-100 motion-reduce:transition-none dark:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0"
        name="name"
        placeholder="Name"
        @input.once="handleClearError('name')"
      />
      <label
        for="name"
        class="pointer-events-none absolute left-3 top-0 mb-0 max-w-[90%] origin-[0_0] truncate pt-[0.37rem] leading-[2.15] text-neutral-500 transition-all duration-200 ease-out peer-focus:-translate-y-[1.15rem] peer-focus:scale-[0.8] peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
      >
        Name
      </label>
    </div>

    <div
      class="relative mb-8"
      data-te-input-wrapper-init
      data-te-validate="input"
      :data-te-invalid-feedback="errors?.email || props.value?.formErrors?.email"
      :data-te-validation-state="errors?.email || props.value?.formErrors?.email ? 'invalid' : 'valid'"
    >
      <input
        v-model="email"
        v-bind="emailProps"
        type="text"
        class="peer block min-h-[auto] w-full rounded border-0 bg-transparent px-3 py-[0.32rem] leading-[2.15] outline-none transition-all duration-200 ease-linear focus:placeholder:opacity-100 data-[te-input-state-active]:placeholder:opacity-100 motion-reduce:transition-none dark:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0"
        name="email"
        placeholder="Email address"
        @input.once="handleClearError('email')"
      />
      <label
        for="email"
        class="pointer-events-none absolute left-3 top-0 mb-0 max-w-[90%] origin-[0_0] truncate pt-[0.37rem] leading-[2.15] text-neutral-500 transition-all duration-200 ease-out peer-focus:-translate-y-[1.15rem] peer-focus:scale-[0.8] peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
      >
        Email Address
      </label>
    </div>

    <div
      class="relative mb-8" 
      data-te-input-wrapper-init
      data-te-validate="input"
      :data-te-invalid-feedback="errors?.password || props.value?.formErrors?.password"
      :data-te-validation-state="errors?.password || props.value?.formErrors?.password ? 'invalid' : 'valid'"
    >
      <input
        v-model="password"
        v-bind="passwordProps"
        type="password"
        class="peer block min-h-[auto] w-full rounded border-0 bg-transparent px-3 py-[0.32rem] leading-[2.15] outline-none transition-all duration-200 ease-linear focus:placeholder:opacity-100 data-[te-input-state-active]:placeholder:opacity-100 motion-reduce:transition-none dark:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0"
        id="password"
        placeholder="Password"
        @input.once="handleClearError('password')"
      />
      <label
        for="password"
        class="pointer-events-none absolute left-3 top-0 mb-0 max-w-[90%] origin-[0_0] truncate pt-[0.37rem] leading-[2.15] text-neutral-500 transition-all duration-200 ease-out peer-focus:-translate-y-[1.15rem] peer-focus:scale-[0.8] peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
      >
        Password
      </label>
    </div>

    <div
      class="relative mb-8" 
      data-te-input-wrapper-init
      data-te-validate="input"
      :data-te-invalid-feedback="errors?.password_confirmation || props.value?.formErrors?.password_confirmation"
      :data-te-validation-state="errors?.password_confirmation || props.value?.formErrors?.password_confirmation ? 'invalid' : 'valid'"
    >
      <input
        v-model="password_confirmation"
        v-bind="passwordConfirmationProps"
        type="password"
        class="peer block min-h-[auto] w-full rounded border-0 bg-transparent px-3 py-[0.32rem] leading-[2.15] outline-none transition-all duration-200 ease-linear focus:placeholder:opacity-100 data-[te-input-state-active]:placeholder:opacity-100 motion-reduce:transition-none dark:text-neutral-200 dark:placeholder:text-neutral-200 [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0"
        name="passwordConfirmation"
        placeholder="Confirm Password"
        @input.once="handleClearError('password_confirmation')"
      />
      <label
        for="passwordConfirmation"
        class="pointer-events-none absolute left-3 top-0 mb-0 max-w-[90%] origin-[0_0] truncate pt-[0.37rem] leading-[2.15] text-neutral-500 transition-all duration-200 ease-out peer-focus:-translate-y-[1.15rem] peer-focus:scale-[0.8] peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[1.15rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
      >
        Confirm Password
      </label>
    </div>

    <button
      type="submit"
      class="inline-block w-full rounded bg-primary px-7 pb-2.5 pt-3 text-sm font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#3b71ca] transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] dark:shadow-[0_4px_9px_-4px_rgba(59,113,202,0.5)] dark:hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)] dark:active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.2),0_4px_18px_0_rgba(59,113,202,0.1)]"
      data-te-submit-btn-ref
      :class="submitDisabled ? 'pointer-events-none disabled:opacity-70' : ''"
      :disabled="submitDisabled"
    >
      Register
    </button>
  </form>
</template>

Reproduction steps

  1. Create register form component
  2. Install and use vee-validate with yup
  3. Setup form validation based on the documentation with yup implementation
  4. Apply learning on integrating the validation on TW-Elements form components
  5. Try to check the behavior of the validation in every form field but only one field(depends on the user's selected field) will trigger the validation, the rest is not showing validation although in the component data the validation error message is showing, it needs to click the button to trigger TW-Element's validation in order to display all the validation set from vee-validate

Version

Vue.js 3.x and vee-validate 4.x

What browsers are you seeing the problem on?

Relevant log output

No response

Demo link

none

Code of Conduct

noelc10 commented 4 months ago

@logaretm

logaretm commented 4 months ago

Doesn't look like it is released:

CleanShot 2024-03-09 at 16 33 36

While I would love to add them to the docs, I can't be adding every single UI lib out there since I will be needing to maintain the examples. If I have time I will do that. Thanks for the suggestion.

noelc10 commented 3 months ago

Doesn't look like it is released:

CleanShot 2024-03-09 at 16 33 36

While I would love to add them to the docs, I can't be adding every single UI lib out there since I will be needing to maintain the examples. If I have time I will do that. Thanks for the suggestion.

Thanks for noticing @logaretm , please do when you have a time, I find this UI kit more similar to Vuetify and the fun on creating components is almost uncanny. Hoping that most of the components is free so we can also do more about it and engage more devs who can find this UI kit awesome.

wishing for more success on this UI kit.