vue icon indicating copy to clipboard operation
vue copied to clipboard

Custom error message from prop validator

Open znck opened this issue 5 years ago • 20 comments

What problem does this feature solve?

Currently, if a custom validator fails, we get a console error log saying Invalid prop: custom validator check failed for prop 'email' which is not helpful if you're using a third-party component. The only way to find out what failed is to jump into the source code of the component and try to understand what does this custom validator do. If the custom validator can provide a custom message that immensely changes developer experience e.g. Instead of Invalid prop: custom validator check failed for prop 'email', it can say, Invalid prop: the prop 'email' should be a valid GMail address.

What does the proposed API look like?

No change in API signature only behavior of validator function. If a validator function throws an error, use it as a custom message for prop validation. Also, allow {{name}} interpolation in error message. So the email can be defined as:

...
  props: {
    email: {
      validator(value) {
        if (!value.endsWith('@gmail.com')) throw new Error('the prop '{{name}}' should be a valid GMail address.')
        return true
     }
   }
...

znck avatar Feb 10 '19 07:02 znck

Duplicate of #8726 tho 😆 I think we should talk about the api a bit first

posva avatar Feb 10 '19 10:02 posva

It was one-liner but I agree on finalizing the API first. With custom validation messages, we can have user-land packages shipping complex validation helpers. I, myself, can use it in znck/prop-types.

znck avatar Feb 10 '19 17:02 znck

FYI: At the moment, there is a way to have a better warning though, one where you can add the value passed to the prop in the warning message:

/**
 * @type {RegExp}
 */
const validatorWarningRegExp = /Invalid prop: custom validator check failed for prop "(.*?)"\./;

/**
 * Better warn messages than the built-ins (adds value for invalid prop, ...).
 *
 * @see {@link https://vuejs.org/v2/api/#warnHandler}
 *
 * @function
 * @param {VueConstructor} vue
 * @param {function(...*)} handler
 */
export const initGlobalWarnHandlerForVue = (vue, handler) => {
  vue.config.warnHandler = (message, vm, trace) => {
    const validatorWarning = validatorWarningRegExp.exec(message);
    if (validatorWarning) {
      handler('[APP] Vue:', message, '\nValue:', vm[validatorWarning[1]], trace);
    } else {
      handler('[APP] Vue:', message, trace);
    }
  };
};

dietergeerts avatar Feb 20 '19 18:02 dietergeerts

When the returned value from a validator is truthy, I wonder if it would be simpler to check if what's returned is a string, then if it is, make that the warning message. Thoughts?

chrisvfritz avatar Mar 12 '19 15:03 chrisvfritz

@chrisvfritz that would break validators that look like val => val.name, i personally expect only the truthiness of a validation result to be checked.

diachedelic avatar Apr 01 '19 06:04 diachedelic

@diachedelic That's true. I like the thrown error better then. 🙂

@posva As for the API, I think it can be discussed separately if throwing an error could be an acceptable solution. Though my personal thought is that unless we're using this for something else in validators, I think it might be most intuitive to just bind props (and $props) directly to the function, so they can be accessed the same way as anywhere else in the component.

chrisvfritz avatar Apr 01 '19 15:04 chrisvfritz

But right now validation takes place in order so not all props are available to validate. I don't think validators should have access to this though

posva avatar Apr 01 '19 16:04 posva

@posva Yes, the change would require postponing prop validation until after the first pass of processing props. Regarding validators having access to this, what are your concerns?

chrisvfritz avatar Apr 01 '19 22:04 chrisvfritz

It would allow people to test against anything related to the component and I don't think we should allow that because a validator should be a pure function. Apart from props and maybe slots presence, what would you use from this

posva avatar Apr 02 '19 20:04 posva

@posva That's it - probably just props and maybe slots. I wouldn't expect the entire instance to be initialized at that point, similar to how not everything is available on this in beforeCreate. When you say "a validator should be a pure function", what are the disadvantages or edge cases you're trying to avoid?

chrisvfritz avatar Apr 04 '19 16:04 chrisvfritz

I want to avoid access to other properties, for example the $router, $store or other things.

posva avatar Apr 04 '19 17:04 posva

This is exactly what is asked on the forum: Props validation error message override

So to resume my point of view:

We could/should be able to do things like this:

props: {
    foobar: {
        type: String,
        validator: ( value ) => { return [ 'foo', 'bar' ].includes( value ) },
        errorMessage: 'Invalid prop "foobar", available values are "foo", "bar".'
    }
}

and the incriminated code with one implementation to that purpose could be:

/**
 * Assert whether a prop is valid.
 */
function assertProp (
    prop,
    name,
    value,
    vm,
    absent
) {
    if (prop.required && absent) {
        if (prop.errorMessage) {
            warn(
                prop.errorMessage,
                vm
            );
        } else {
            warn(
                'Missing required prop: "' + name + '"',
                vm
            );
        }
        return
    }
    if (value == null && !prop.required) {
        return
    }
    var type = prop.type;
    var valid = !type || type === true;
    var expectedTypes = [];
    if (type) {
        if (!Array.isArray(type)) {
            type = [type];
        }
        for (var i = 0; i < type.length && !valid; i++) {
            var assertedType = assertType(value, type[i]);
            expectedTypes.push(assertedType.expectedType || '');
            valid = assertedType.valid;
        }
    }
    if (!valid) {
        if (prop.errorMessage) {
            warn(
                prop.errorMessage,
                vm
            );
        } else {
            warn(
                "Invalid prop: type check failed for prop \"" + name + "\"." +
                " Expected " + (expectedTypes.map(capitalize).join(', ')) +
                ", got " + (toRawType(value)) + ".",
                vm
            );
        }
        return
    }
    var validator = prop.validator;
    if (validator) {
        if (!validator(value)) {
            if (prop.errorMessage) {
                warn(
                    prop.errorMessage,
                    vm
                );
            } else {
                warn(
                    'Invalid prop: custom validator check failed for prop "' + name + '".',
                    vm
                );
            }
        }
    }
}

So what about a such implementation ?

Itee avatar Jul 01 '19 22:07 Itee

I think the validator method should pass some kind of a reject function that let the developer throw an error according to the validation. Or the validator function could return true, false, or a string that will be displayed as the error message. The errorMessage property you propose is too limiting in my opinion.

nicooprat avatar Sep 16 '19 14:09 nicooprat

Why not let the validator return the error message? If result is a function call the function, this allows custom logging nicely, if it returns a string use it as the message. Otherwise return the default message.

rahicks26 avatar Dec 27 '19 03:12 rahicks26

errorMessage is definitely too limiting. e.g. I'm coding composable run-time type validators for deep objects where I need to report a validation error on a nested value in a complex Object prop (I forked and adapted a lib for use in Vue).

@rahicks26 I agree that just returning a string as validation error would be preferred BUT sadly it's not backward compatible with the existing API so I think throwing an exception is the only option. Its also safer since your custom validator may itself have a bug that throws an exception.

tohagan avatar Jan 24 '20 05:01 tohagan

This doubles the console messages, but you can currently just console.warn or console.error from within existing validators.

validator(value) {
  const isValid = !!<what you’d normally return from your validator>
  if (!isValid) {
    console.warn(`${value} is not valid`)
  }
  return isValid
}

aminimalanimal avatar May 14 '20 22:05 aminimalanimal

The simplest way for now to print a custom error message and not produce a double error is to raise a console error (in my case I don't want to throw an Error) and return true in the validator function. Like in this example:

selectableRows: {
      validator: value => {
        if (![undefined, true, false, 1, '1', ''].includes(value)) {
          console.error(
            'Wrong value for the `selectableRows` prop. ' +
            `Given: "${value}", expected one of: [undefined, true, false, 1, '1', ''].`
          )
        }
        return true
      }
    }

antoniandre avatar Mar 28 '21 17:03 antoniandre

The simplest way for now to print a custom error message and not produce a double error is to raise a console error (in my case I don't want to throw an Error) and return true in the validator function. Like in this example:

selectableRows: {
      validator: value => {
        if (![undefined, true, false, 1, '1', ''].includes(value)) {
          console.error(
            'Wrong value for the `selectableRows` prop. ' +
            `Given: "${value}", expected one of: [undefined, true, false, 1, '1', ''].`
          )
        }
        return true
      }
    }

Whilst this does work, it still doesn't feel like it should be the way to achieve this. Logging an error could be useful (in an isolated context within a custom validator) but I would guess most who are writing code that is going to production shouldn't be adding console.error in places.

My use case for needing custom error messages is for both cross prop validation and validation on itself. I am the author of a design system and therefore I am concerned about consumers/other developers being aware of prop validation when in development. I need to avoid errors to leak to anywhere but development (Vue warn won't do this).

I have had to create this which I am using in a props watch handler with immediate: true to be able to validate a component's props against each other:

export function validateVueProps(propCondition, errorMessage) {
  if (propCondition && process.env.NODE_ENV !== "production") {
    throw new TypeError(`${errorMessage}`);
  }
}

My second use case is having a custom validator for prop type of Array. I want to specify a value and label key that exists in the passed Array. It would be nice to display custom message - "Your options must contain a label and value", for example. A similar thing referenced here https://github.com/vuejs/vue/issues/6496#issuecomment-922364934. e.g.

validator: (options) => options.map(key => Object.keys(key)).includes("label" && "value")

A generic [Vue warn] on quite specific validation seems antithetical.

My specific use cases (I appreciate cross prop validation is not really on topic here) shouldn't necessarily deter from the idea that:

  • Vue supplies a way of passing in custom validators. This could determine any behaviour you like given that prop.
  • A [Vue warn] is useful insofar that it tells you something that has happened but not why. Surely only half the way there for the reason you wanted to write a custom validator in the first place? I am currently relying on writing JSDoc in the component to specify prop behaviour.
  • Relying on [Vue warn] is a good thing in that the warnHandler is turned off for "production".

Just my thoughts, thanks all.

Dawdre avatar Jan 27 '22 16:01 Dawdre

Is this still in progress?

vinceramcesoliveros avatar Mar 05 '22 07:03 vinceramcesoliveros

+1

ddenev avatar May 17 '22 17:05 ddenev