Skip to content
This repository has been archived by the owner on May 6, 2021. It is now read-only.

Latest commit

 

History

History
359 lines (279 loc) · 13.8 KB

tutorial-binder-validation.asciidoc

File metadata and controls

359 lines (279 loc) · 13.8 KB
title order layout
Validating User Input
3
page

Validating User Input

Vaadin helps you validate user input based on the backend Java data model. It reads the Bean Validation (JSR-380) annotations on your Java data types and applies these constraints to the user input. Validation is enabled by default in all forms created with the Vaadin Binder API.

When creating forms in TypeScript, with LitElement and the Binder API, you automatically get all data model constraints applied to your form fields. The Binder API validates most standard constraints such as @Max, @Size, @Pattern, @Email on the client-side, without a network round trip delay (see the full list in the Built-in Client-Side Validators section below). When you eventually submit data to server-side endpoints, Vaadin validates all constraints on the server as well, and the Binder API would update the form to show server-side validation errors (if any).

This chapter describes input validation for forms created with TypeScript and LitElement (the @vaadin/form module). For details on input validation for forms created in Java see the Validating and Converting User Input chapter.

Specifying Constraints

Constraints are specified as a part of the data model, in the Java code. You can use any of the built-in constraints as well as your own.

Defining constraints on individual object properties using the Bean Validation annotations
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

public class Employee {
    @NotBlank
    private String username;

    private String title;

    @Email(message = "Please enter a valid e-mail address")
    private String email;

    // + other fields, constructors, setters and getters
}

During the build, when Vaadin generates TypeScript types, it includes the constraint information into the generated model types. All forms working with the same entity type have the same set of constraints.

When you create a form for an entity type it gets the user input validation automatically.

Binding form fields to the data model
import { Binder, field } from '@vaadin/form';
import EmployeeModel from './generated/com/example/application/EmployeeModel';

...

private binder = new Binder(this, EmployeeModel);

render() {
  const { model } = this.binder;

  return html`
    <vaadin-text-field label="Username"
      ...=${field(model.username)}></vaadin-text-field>
    <vaadin-text-field label="Title"
      ...=${field(model.title)}></vaadin-text-field>
    <vaadin-email-field label="Email"
      ...=${field(model.email)}></vaadin-email-field>
  `;
}

The validation errors

Defining Custom Constraints

The Bean Validation standard allows creating arbitrary custom constrains. The Vaadin form Binder API supports such custom constraints as well. The example below shows how to create and use a custom @StrongPassword constraint:

Defining a custom @StrongPassword constraint
@Retention(RUNTIME)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Constraint(validatedBy = { StrongPasswordValidator.class })
public @interface StrongPassword {

    // min required password strength on the scale from 1 to 5
    int minStrength() default 4;

    String message() default "Please enter a strong password";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}
Defining a validator for the custom @StrongPassword constraint
public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        // Use the zxcvbn library to measure the password strength
        Strength strength = zxcvbn.measure(object);

        // fail the validation if the measured strength is insufficient
        if (strength.getScore() < minStrength) {
            constraintContext
                .buildConstraintViolationWithTemplate(
                        strength.getFeedback().getWarning())
                .addConstraintViolation();
            return false;
        }

        return true;
    }
}

In this example we use a 3rd party library for measuring password strength in order to implement the custom validator. Add a dependency to your pom.xml:

<dependency>
    <groupId>com.nulab-inc</groupId>
    <artifactId>zxcvbn</artifactId>
    <version>1.3.0</version>
</dependency>
Using the custom @StrongPassword constraint in a data type
public class Employee {

    @StrongPassword
    private String password;
}

No additional steps are needed in order to start using the new validation rules in forms. The field() directive applies all server-side constraints automatically.

profile-view.ts
private binder = new Binder(this, EmployeeModel);

render() {
  const { model } = this.binder;

  return html`
    <vaadin-password-field label="Password"
      ...=${field(model.password)}></vaadin-password-field>

    <vaadin-button @click="${this.save}">Save</vaadin-button>
  `;
}

Notice however, that in this example validation happens only after the form is submitted. In order to validate the user input immediately, as users type, you would need to define a validator in TypeScript as well. The following section shows how to do that.

Defining Custom Client-Side Validators

In order to give instant feedback to users as they type, you can define validators in TypeScript so that they are executed in the browser, without a network round trip. The Vaadin form Binder API allows adding validators both for individual fields, and for the entire form value as a whole (e.g. to implement cross-field validation). Client-side validators are executed before the server-side is invoked.

Warning
Validation ALWAYS needs to run on the server for your application to be secure. Additionally, you may validate input in the browser—​immediately as users type—​to give a better user experience.

Adding Validators for a Single Field

When a validation rule concerns a single field, a client-side validator should be added with the addValidator() call on the binder node for that particular field. This is the case with the custom @StrongPassword constraint example.

Custom Field Validation Error

profile-view.ts
import * as owasp from 'owasp-password-strength-test';

// binder.for() returns a binder for the password field
const model = this.binder.model;
this.binder.for(model.password).addValidator({
  message: 'Please enter a strong password',
  validate: (password: string) => {
    const result = owasp.test(password);
    if (result.strong) {
      return true;
    }
    return { property: model.password, message: result.errors[0] };
  },
});

In this example we use a 3rd party library for measuring password strength in order to implement the custom validator. Add a dependency to your package.json:

npm install --save owasp-password-strength-test
npm install --save-dev @types/owasp-password-strength-test

Adding Cross-Field Validators

When a validation rule is based on several fields, a client-side validator should be added with the addValidator() call on the form binder directly. A typical example where this would be needed is, checking that password is repeated correctly:

Custom Field Validation Error

private binder = new Binder(this, EmployeeModel);

render() {
  return html`
    <vaadin-password-field label="Password"
      ...=${field(model.password)}></vaadin-password-field>
    <vaadin-password-field label="Repeat password"
      ...=${field(model.repeatPassword)}></vaadin-password-field>
  `;
}

protected firstUpdated(_changedProperties: any) {
  super.firstUpdated(args);

  const model = this.binder.model;
  this.binder.addValidator({
    message: 'Please check that the password is repeated correctly',
    validate: (value: Employee) => {
      if (value.password != value.repeatPassword) {
        return [{ property: model.password }];
      }
      return [];
    }
  });
}

When record-level validation fails, there are cases when you want to mark several fields as invalid. In order to do that with the @vaadin/form validator APIs, you can return an array of { property, message } records from the validate() callback. Returning an empty array would be equivalent to returning true, i.e. validation would pass. In case if you need to indicate a validation failure without marking any particular field as invalid, return false.

Marking Fields as Required

In order to mark a form field as 'required', you can add a @NotNull or @NotEmpty constraints to the corresponding property in the Java type. @Size with a min value greater than 0 makes a field required as well.

Alternatively, you can set the impliesRequired property when adding a custom validator in TypeScript as shown in the Adding Validators for a Single Field section above.

The fields marked as required get the required property set by the field() directive, and cause validation failure if left empty.

Built-in Client-Side Validators

The @vaadin/form package provides the client side validators for the following JSR-380 built-in constraints:

  1. Email - The string has to be a well-formed email address.

  2. Null - Must be null

  3. NotNull - Must not be null

  4. NotEmpty - Must not be null nor empty (must have a length property, e.g. string or array)

  5. NotBlank - Must not be null and must contain at least one non-whitespace character

  6. AssertTrue - Must be true

  7. AssertFalse - Must be false

  8. Min - Must be a number whose value must be higher or equal to the specified minimum

    • Additional options: { value: number | string }

  9. Max - Must be a number whose value must be lower or equal to the specified maximum

    • Additional options: { value: number | string }

  10. DecimalMin - Must be a number whose value must be higher or equal to the specified minimum

    • Additional options: { value: number | string, inclusive: boolean | undefined }

  11. DecimalMax - Must be a number whose value must be lower or equal to the specified maximum

    • Additional options: { value: number | string, inclusive: boolean | undefined }

  12. Negative - Must be a strictly negative number (i.e. 0 is considered as an invalid value)

  13. NegativeOrZero - Must be a negative number or 0

  14. Positive - Must be a strictly positive number (i.e. 0 is considered as an invalid value)

  15. PositiveOrZero - Must be a positive number or 0

  16. Size - Size must be between the specified boundaries (included; must have a length property, e.g. string or array)

    • Additional options: { min?: number, max?: number }

  17. Digits - Must be a number within accepted range

    • Additional options: { integer: number, fraction: number }

  18. Past - A date string in the past

  19. PastOrPresent - A date string in the past or present

  20. Future - A date string in the future

  21. FutureOrPresent - A date string in the future or present

  22. Pattern - Must match the specified regular expression

    • Additional options: { regexp: RegExp | string }

In most cases they are used automatically. However, you could also add them to selected fields manually with binder.for(myFieldModel).addValidator(validator). E.g. addValidator(new Size({max: 10, message: 'Must be 10 characters or less'})).

All of the built-in validators take one constructor parameter which is usually an optional options object with a message?: string property (which defaults to 'invalid'), but some validators have additional options or support other argument types instead of the options object.

For example the Min validator requires a value: number | string option which may be given as part of the options object or you can pass just the minimum value itself instead of the options object (if you don’t want to set message and leave it as the default 'invalid').

import { Binder, field, NotEmpty, Min, Size, Email } from '@vaadin/form';

@customElement('my-demo-view')
export class MyDemoView extends LitElement {
  private binder = new Binder(this, PersonModel);

  protected firstUpdated(_changedProperties: any) {
    super.firstUpdated(args);

    const model = this.binder.model;

    this.binder.for(model.name).addValidator(
      new NotEmpty({
        message: 'Please enter a name'
      }));

    this.binder.for(model.username).addValidator(
      new Size({
        message: 'Please pick a username 3 to 15 symbols long',
        min: 3,
        max: 15
      }));

    this.binder.for(model.age).addValidator(
      new Min({
        message: 'Please enter an age of 18 or above',
        value: 18
      }));

    this.binder.for(model.email).addValidator(new Email());
  }

  render() {
    const model = this.binder.model;
    return html`
      <vaadin-text-field label="Name"
        ...="${field(model.name)}"></vaadin-text-field>
      <vaadin-text-field label="Username"
        ...="${field(model.username)}"></vaadin-text-field>
      <vaadin-integer-field label="Age"
        ...="${field(model.age)}"></vaadin-integer-field>
      <vaadin-email-field label="Email"
        ...="${field(model.email)}"></vaadin-email-field>
    `;
  }
}