Use Vue modifiers to make form handling with Vuetify easier

Use Vue modifiers to make form handling with Vuetify easier

Vue contains two very convenient v-model modifiers for trim and number. Using them means that the models are only updated after the trim or number conversion are done, which means we don't have to do those things ourself anymore.

As an example let's say we have a form with the fields firstName, lastName and experienceInYears. For stricter typing I use the following interface.

export interface StrictFormDefinition {
  valid: boolean;
  fields?: {
    [key: string]: FormField<any>;
  };
}

export interface FormField<T> {
  id?: string;
  value: T;
  rules: FormValidationRule<T>[];
}

export type FormValidationRule<T> = (value: T) => boolean | string;

In our component we extend that to formulate the following definition.

interface Form extends StrictFormDefinition {
  fields: {
    firstName: FormField<string>;
    lastName: FormField<string>;
    experienceInYears: FormField<string>;
  }
}

All of them have to be of type string as even if we're using <v-text-field type="number" /> the returned value is still a string.

Our action is typed as the following command:

export interface UpdateUserCommand extends Command {
  firstName?: string;
  lastName?: string;
  experienceInYears?: number;
}

So all fields are optional. The function to create and submit such a command would look like the following.

onSubmit(): void {
  const command: UpdateUserCommand = {};

  if (this.form.fields.firstName.value.trim().length > 0) {
    command.firstName = this.form.fields.firstName.value.trim();
  }
  if (this.form.fields.lastName.value.trim().length > 0) {
    command.firstName = this.form.fields.lastName.value.trim();
  }
  if (this.form.fields.experienceInYears.value.trim().length > 0) {
    command.firstName = parseInt(this.form.fields.experienceInYears.value.trim(), 10);
  }

  this.updateUser(command)
    .then(() => {
       ...

When using the modifiers trim and number, like this:

...
<v-text-field type="text" v-model.trim="form.fields.firstName.value" :rules="form.fields.firstName.rules" />
<v-text-field type="text" v-model.trim="form.fields.lastName.value" :rules="form.fields.lastName.rules" />
<v-text-field type="number" v-model.trim.number="form.fields.experienceInYears.value" :rules="form.fields.experienceInYears.rules" />
...

We can adapt the form definition slightly to the following:

interface Form extends StrictFormDefinition {
  fields: {
    firstName: FormField<string>;
    lastName: FormField<string>;
    experienceInYears: FormField<number|string>;
  }
}

The greater change is in our command creation:

onSubmit(): void {
  const command: UpdateUserCommand = {};

  if (this.form.fields.firstName.value.length > 0) {
    command.firstName = this.form.fields.firstName.value;
  }
  if (this.form.fields.lastName.value.length > 0) {
    command.firstName = this.form.fields.lastName.value;
  }
  if (isNumber(this.form.fields.experienceInYears.value)) {
    command.firstName = this.form.fields.experienceInYears.value as number;
  }

  this.updateUser(command)
    .then(() => {
       ...

This way we have less "cleanup code" in our logic which becomes more readable!