Ion Item validation color doesn't work in Vue

Per the ion-item documentation the highlight color of an item should change based on the validity of the inputs inside of it.

This however does not appear to be working in Vue. Is this planned to be supported? Should I open a bug on this?

I created a temporary hack/fix for myself. Posting it in case anyone runs into something similar. Presently this only works for ion-inputs as far as I know. I’ll post the actual file at the bottom since it’s big, but you use it like this:

Here’s validation on a first name, last name, and email:

<ion-item>
  <ion-label position="floating">First Name</ion-label>
  <ion-input ref="firstName" required="true" v-model="profile.firstName" @ionBlur="validateInput('firstName')"> </ion-input>
</ion-item>
<span class="validation-error" v-if="validityStates?.firstName?.valueMissing">First name is required.</span>

<ion-item>
  <ion-label position="floating">Last Name</ion-label>
  <ion-input ref="lastName" required="true" v-model="profile.lastName" @ionBlur="validateInput('lastName')"> </ion-input>
</ion-item>
<span class="validation-error" v-if="validityStates?.lastName?.valueMissing">Last name is required.</span>

<ion-item>
  <ion-label position="floating">Email</ion-label>
  <ion-input ref="email" required="true" type="email" v-model="profile.email" @ionBlur="validateInput('email')"> </ion-input>
</ion-item>
<span class="validation-error" v-if="validityStates?.email?.valueMissing">Email address is required.</span>
<span class="validation-error" v-if="validityStates?.email?.typeMismatch">Please enter a valid email address</span>

Any input I want to validate I must give a ref. Then on blur, i call validateInput, pasing the name of my ref as a string. To show more details about the error message, I have a span with a v-if which checks my validityStates object, which is a key / value object where the key is the ref name of the input, and the value is a standard web ValidityState object. I can then check any number of validations that the web supports natively.

I follow this same pattern for last name, and for email, and with email I check both that the value is present and that it is a valid email address format.

In my save method in my component, I also check validation of all inputs, and pop a toast if any are invalid. That way when they press save, any invalid fields will underline in red and show their error message.

async save() {
  if (!this.validateAll()) {
    showErrorToast('Please fill out all required fields');
    return;
  }

  try {
    loadingSpinnerService.present();
    //... rest of save logic here

To wire this up, you also have to import and use the function I’ve created in your setup block like so:

import { setupIonicFormValidation } from '@/shared/validation';

export default defineComponent({
  name: 'ProfileSettings',
  setup() {
    return {
      ...setupIonicFormValidation('firstName', 'lastName', 'email'),
    }

You must pass the function each ref name you want validated as a string. I want inputs with refs ‘firstName’, ‘lastName’, and ‘email’ validated, so I have to pass those into my function.

That’s it. This will validate form fields both on save, and on blur.

I guess I could toss this in an npm package for easy reuse, but not sure the demand, plus ionic will probably fix this at some point. In the mean time though, toss the following typescript code in a file and use it like shown above, and it works:

//Inside shared/validation.ts
import { ref } from 'vue';

interface ValidationTools {
  validityStates: { [fieldRefName: string]: ValidityState };
  validateInput(formFieldRefName: string): ValidityState;
  validateAll(): boolean;
}

export function setupIonicFormValidation(...formFieldRefNames: string[]): ValidationTools {
  const validityStates = ref({} as any);

  const toValidate = {} as any;
  formFieldRefNames.forEach(refName => toValidate[refName] = ref(null));

  const validateInput = (formFieldRefName: string): ValidityState => {
    const elementRef = toValidate[formFieldRefName].value.$el as HTMLElement;
    const validityState = (elementRef.children[0] as any).validity as ValidityState;
    validityStates.value[formFieldRefName] = ref(validityState);
    if (validityState.valid) {
      elementRef.parentElement?.classList.remove('ion-invalid');
    } else {
      elementRef.parentElement?.classList.add('ion-touched', 'ion-invalid');
    }
    
    return validityState;
  }

  const validateAll = (): boolean => {
    let isAllValid = true;
    Object.keys(toValidate).forEach(keyToValidate => {
      const validityState = validateInput(keyToValidate);
      if(!validityState.valid) {
        isAllValid = false;
      }
    });

    return isAllValid;
  }

  return {
    validityStates,
    ...toValidate,
    validateInput,
    validateAll,
  }
}