Skip to content

Validators

Michael Brandt edited this page Sep 14, 2024 · 10 revisions

On this page

Validators

Validators are functions that takes a value to validate and returns undefined if there is no error and an error message (string) if there is an error.

You can use the builtin validators from simple-form or your own or a combination.

How to use the builtin validators

The builtin validators are available with the useValidationRules hook like.

const { required, maxLength } = useValidationRules();

The builtin validators are functions that returns a Validator<T> which in itself is a function, so when you use a validator in a form definition, you must invoke it.

const fd = useFormDefinition<FormFields>({
  fields: {
    name: {
      validators: [required(), maxLength(50)]
    },
    age: {
      validators: [required()]
    }
  }
});

Some validators takes some configuration parameters, but they all take an optional errorMessage as the last parameter. If you call them without the errorMessage parameter, they use the error messages from simple-form, but you have the opportunity to override it with a custom message.

const fd = useFormDefinition<FormFields>({
  fields: {
    name: {
-      validators: [required(), maxLength(50)]
+      validators: [required("Please tell us your name."), maxLength(50, "Oops no more than 50 characters.)]
    },
    age: {
-      validators: [required()]
+      validators: [required("Enter your age.")]
    }
  }
});

The alwaysValid validator

simple-form exposes an alwaysValid validator which always returns undefined meaning no error.

This can come in handy when constructing an array of validators dynamically.

Assume our component has a prop telling if age is a required field.

type Props = {
  ageRequired: boolean;
};

const PersonForm = (props: Props) => {
  const { ageRequired } = props;
  //... the rest of the component
}

The age field is defined like this so far:

  age: {
    validators: [required(), min(3), max(125)]
  },

We only want to include the required validator if ageRequired is true.

  age: {
-   validators: [required(), min(3), max(125)]
+   validators: ageRequired ? [required(), min(3), max(125)] : [min(3), max(125)]
  },

This works, but as you can see we repeat the min(3), max(125) validators because we want this regardless of ageRequired. If we had more validators we could easily forget one in the two situations. Instead we can use alwaysValid which makes it easier to create the array.

  age: {
-   validators: [required(), min(3), max(125)]
+   validators: [ageRequired ? required() : alwaysValid, min(3), max(125)]
  },

Now we only specify the other validators one time.

Note alwaysValid is not a function like the rest of the validators. This is because the other validators are functions so they can take a custom error message, but as alwaysValid never returns an error, it can be just a constant.

Validating boolean values

A common use case is to have a checkbox for something like Please accept our terms. You want the user to check this checkbox before submitting the form.

This can be accomplished with the equal validator.

const fd = useFormDefinition<FormFields>({
  fields: {
    name: {
      validators: [required(), maxLength(50)]
    },
    age: {
      validators: [min(3), max(125)]
    },
    acceptTerms: {
      validators: [required("Please accept the terms."), equal(true, "Please accept the terms")]
    }
  }
});
<Box display="flex" flexDirection="column" alignItems="flex-start" gap={1}>
  <FormText formManager={fm} fieldName="name" label="Name" />
  <FormNumber formManager={fm} fieldName="age" label="Age" />
  <FormCheckbox formManager={fm} fieldName="acceptTerms" label="Please accept our terms" />
  <Button onClick={handleSubmit}>SUBMIT</Button>
</Box>

We must also use the required validator because all other validators should not report an error for a null value.

Equal validator

Validating related fields

It is easy to create validation rules that ensures some relation between two or more fields. Let's create an example where the user should enter a persons working hours.

type FormFields = {
  fromHours: number;
  toHours: number;
};
const defaultValidators = [required(), min(5), max(23)];

const fd = useFormDefinition<FormFields>({
  fields: {
    fromHours: {
      validators: defaultValidators
    },
    toHours: {
      validators: defaultValidators
    }
  }
});
<Box display="flex" flexDirection="column" alignItems="flex-start" gap={1}>
  <FormNumber formManager={fm} fieldName="fromHours" label="From hours" />
  <FormNumber formManager={fm} fieldName="toHours" label="To hours" />
  <Button onClick={handleSubmit}>SUBMIT</Button>
</Box>

Related fields

Both values should be between 5 and 23 and we also want fromHours to be less than toHours.

fm.onChange.fromHours = value => {
  fm.setValidators("toHours", [...defaultValidators, hasValue(value) ? min(value + 1) : alwaysValid]);
};

fm.onChange.toHours = value => {
  fm.setValidators("fromHours", [...defaultValidators, hasValue(value) ? max(value - 1) : alwaysValid]);
};

If the user enters 8 in fromHours and then 7 in toHours the validators reports these errors.

Related fields validators

This may look a little intrusive. We can make it better by having custom error messages.

fm.onChange.fromHours = value => {
  fm.setValidators("toHours", [...defaultValidators, hasValue(value) ? min(value + 1, "To hours must be greater than From hours.") : alwaysValid]);
};

fm.onChange.toHours = value => {
  fm.setValidators("fromHours", [...defaultValidators, hasValue(value) ? max(value - 1, "From hours must be less than To hours.") : alwaysValid]);
};

Related fields validators

This example also shows some other useful concepts. We have some default validation rules for each field and we don't want to duplicate them in the useFormDefinition call and in the fm.onChange event handler. To avoid this, we create a local variable defaultValidators and use it in both cases of the ternary expression.

Also note that we set the min validator twice for the toHours field. The first time min(5) ensures that the user can never enter a value below 5 and the second ensures that the value is less than the value of the toHours field. When validation is performed, it occurs in the order the validators appears in the array and when an error is found, validation for that field is terminated.

If the user enters 7 in toHours the validators for fromHours would be:

[required(), min(5), max(23), min(6)]

This could of course also be accomplished by only using min once and calculate the minimum value Math.max(5, 7-1), but this wouldn't make it possible to reuse defaultValidators.

Custom validation rules

If the validators from simple-form aren't sufficient, you can easily create your own.

Your validator must be a Validator<T> where T is the type it is validating - this ensures you can't by mistake apply it to a field of another type than T.

The validator should return undefined if there are no errors and an error message otherwise. The validator should not return any errors if the value is null (because this should be handled by the required validator).

Say you only want to allow odd numbers for a field.

const odd: Validator<number> = value => (hasValue(value) && value % 2 !== 0 ? "Must be an odd number" : undefined);

You can then use it like this:

mustBeOdd: {
  validators: [odd]
}

If you are exporting the validator and intent to use it in other places, you should create it like the builtin validators, taking a parameter for an optional custom error message.

const odd =
  (errorMessage?: string): Validator<number> =>
  value =>
    hasValue(value) && value % 2 !== 0 ? errorMessage ?? "Must be an odd number" : undefined;

And then invoke it when you use it:

mustBeOdd: {
  validators: [odd()]
}

Special note on required validator

Have you noticed that you don't have to tell the form components when the value is required other than using the required validator? How does simple-form find out that among the array of validators (which are functions) one of them is the required validator? It does so by looking at the name of the function. This is the reason the required validator is implemented using function syntax and not arrow syntax.

const required = <T>(errorMessage?: string): Validator<T> =>
  // Must be written like this because the name of the function returned must start with "required".
  function required(value) {
    return hasValue(value) ? undefined : errorMessage ?? "Value is required";
  };

You can create your own validators for required (for instance I use a special version for usage with Facebook's draft editor). The only requirement is that you implement it as above and that your function starts with "required" in the name.