Categories
Vue 3

Vue Router 4–404 and Nested Routes

Vue Router 4 is in beta and it’s subject to change.

To build a single page app easily, we got to add routing so that URLs will be mapped to components that are rendered.

In this article, we’ll look at how to use Vue Router 4 with Vue 3.

Catch-All / 404 Not Found Route

We can create a catch-all or 404 route by using the asterisk pattern as the path.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo">Foo</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>"
      };

      const NotFound = {
        template: "<div>not found</div>"
      };

      const routes = [
        { path: "/foo", component: Foo },
        { path: "/:catchAll(.*)", component: NotFound }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We define a catch-all route with the /:catchAll(.*) capture group instead of * as in Vue Router 3.

Now when we go to any path that isn’t foo , we’ll see the ‘not found’ message.

Nested Routes

We can create nested routes with the children property.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/parent/home">parent</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Parent = {
        template: `<div>
          parent
          <router-view></router-view>
        </div>`
      };

      const Home = {
        template: `<div>home</div>`
      };

       const routes = [
        {
          path: "/parent",
          component: Parent,
          children: [{ path: "home", component: Home }]
        }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        watch: {
          $route() {
            console.log(this.$route.resolve);
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have a routes array with an object that has the children property.

The property has an object with the path and component properties like a regular route.

In the router-link , the to prop has the path to the nested route.

The Parent component has the router-view component so that we can view the content of the child route.

Therefore, when we click on the parent link, we see the:

parent
home

text displayed.

We can have URL parameters in the parent route.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/parent/1/home">parent</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Parent = {
        template: `<div>
          <div>parent {{ $router.currentRoute.value.params.id }}</div>
          <div><router-view></router-view></div>
        </div>`
      };

      const Home = {
        template: `<div>home</div>`
      };

      const routes = [
        {
          path: "/parent/:id",
          component: Parent,
          children: [{ path: "home", component: Home }]
        }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        watch: {
          $route() {
            console.log(this.$route.resolve);
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

To add the :id URL parameter placeholder.

Since the router-link ‘s to prop is now /parent/1/home and we have the $router.currentRoute.value.params.id in the Parent ‘s template, we’ll see the number 1 displayed.

The URL parameter placeholder of the parent stays with the parent.

Conclusion

The way we define 404 and nested routes are slightly different from Vue Router 3 in Vue Router 4.

Categories
Vue

Vuelidate — More Complex Custom Validators

Vue.js doesn’t come with any form validation capabilities by default.

Therefore, we need to add our own form validation library or with our own code.

In this article, we’ll look at how to create custom validators with Vuelidate.

Accessing Component

We can access the whole component’s model with Vuelidate.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.mustBeCool">name is invalid.</div>
    </div>

<div>
      <label>field</label>
      <input v-model.trim="$v.field.$model">
    </div>
  </div>
</template>
<script>
import { helpers } from "vuelidate/lib/validators";

const contains = param =>
  helpers.withParams(
    { type: "contains", value: param },
    (value, vm) => !helpers.req(value) || vm[param.field].includes(param.text)
  );
export default {
  name: "App",
  data() {
    return {
      name: "",
      field: ""
    };
  },
  validations: {
    name: {
      mustBeCool: contains({ text: "cool", field: "field" })
    },
    field: {}
  }
};
</script>

to add a validator that checks if the field field has the word 'cool' in it.

Is it is, then the field that uses the contains validator is valid if the field field has the word 'cool' in it.

vm has the component’s view model, which has the states.

Regex Based Validator

We can add validation based on regex.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.alpha">name is invalid.</div>
    </div>
  </div>
</template>
<script>
import { helpers } from "vuelidate/lib/validators";

const alpha = helpers.regex("alpha", /^[a-zA-Z]*$/);

export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      alpha
    }
  }
};
</script>

We added the alpha validator to check if the inputted value is all alphabetical.

Locator Based Validator

Also, we can validate with the locator strategy.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.password.$model">
    </div>

    <div>
      <label>confirm password</label>
      <input v-model.trim="$v.confirmPassword.$model">
      <div v-if="!$v.confirmPassword.same">confirm pasword must be the same as password.</div>
    </div>
  </div>
</template>
<script>
import { helpers } from "vuelidate/lib/validators";

const sameAs = equalTo =>
  helpers.withParams({ type: "sameAs", eq: equalTo }, function(
    value,
    parentVm
  ) {
    return value === helpers.ref(equalTo, this, parentVm);
  });

export default {
  name: "App",
  data() {
    return {
      password: "",
      confirmPassword: ""
    };
  },
  validations: {
    password: {},
    confirmPassword: {
      same: sameAs("password")
    }
  }
};
</script>

The helper.ref method checks if 2 fields are equal to each other with the equalTo parameter.

The equalTo parameter has the field name to check for in order to determine if it’s the same as that field.

So equalTo would be 'password' since we want to check if it’s the same as 'password' .

We return the condition to compare if the other field’s value is the same as what we inputted into the given field.

Conclusion

We can create our own custom validators with various strategies with Vuelidate.

Categories
Vue

Vuelidate — Dynamic Validation

Vue.js doesn’t come with any form validation capabilities by default.

Therefore, we need to add our own form validation library or with our own code.

In this article, we’ll look at how to validate forms dynamically with Vuelidate.

Dynamic Validation Schema

We can validate forms dynamically with Vuelidate.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>Name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.required">Name is required.</div>
    </div>

<div v-if="hasDescription">
      <label>Description</label>
      <input v-model.trim="$v.description.$model">
      <div v-if="!$v.description.required">Description is required.</div>
    </div>

<div>
      <label>Has Description</label>
      <input v-model="hasDescription" type="checkbox">
    </div>
  </div>
</template>
<script>
import { required } from "vuelidate/lib/validators";

export default {
  name: "App",
  data() {
    return {
      hasDescription: false,
      name: "",
      description: ""
    };
  },
  validations() {
    if (!this.hasDescription) {
      return {
        name: {
          required
        }
      };
    } else {
      return {
        name: {
          required
        },
        description: {
          required
        }
      };
    }
  }
};
</script>

We have the validations method to return an object with the fields to validate according to the this.hasDescription property.

In the template, we need to check the value if the hasDescription and only render the description field if hasDescription is true .

hasDescription ‘s value is controlled by the checkbox.

Dynamic Parameters

We can also set the name of the field dynamically.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>Name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name[valName]">Name is invalid.</div>
    </div>
  </div>
</template>
<script>
import { minLength } from "vuelidate/lib/validators";

export default {
  name: "App",
  data() {
    return {
      name: "",
      minLength: 3,
      valName: "validatorName"
    };
  },
  validations() {
    return {
      name: {
        [this.valName]: minLength(this.minLength)
      }
    };
  }
};
</script>

We have a dynamic name for the validator for the name field.

This can be used in the template to check for the name.

Builtin Validators

There are various kinds of validators that comes with Vuelidate.

They include:

  • required — checks if a field is required.
  • requiredIf — a field is required given that the predicate is true
  • requiredUnless — a field is required given that the predicate is false
  • minLength — minimum length of input is required
  • maxLength — max length of input required
  • minValue — min numeric value
  • maxValue — max numeric value
  • between — numeric range
  • alpha — alphabetical characters
  • alphaNum — alphanumeric characters
  • numeric — numbers
  • integer — integers
  • decimal — decimal numbers
  • email — email address
  • ipAddress — IP address
  • macAddress — MAC address
  • sameAs — checks if a field has the same value as another field
  • url — URL
  • or — passes when at least one of the validator passes
  • and — passes when all validators pass
  • not — passes when the provided validator doesn’t pass
  • withParams — validator modifier for creating custom validator

For instance, we can write:

<template>
  <div id="app">
    <div>
      <label>field</label>
      <input v-model.trim="$v.field.$model">
      <div v-if="!$v.field.required">field is invalid.</div>
    </div>

    <div>
      <label>is optional</label>
      <input v-model="isOptional" type="checkbox">
    </div>
  </div>
</template>
<script>
import { requiredUnless } from "vuelidate/lib/validators";

export default {
  name: "App",
  data() {
    return {
      field: "foo",
      isOptional: true
    };
  },
  validations: {
    field: {
      required: requiredUnless("isOptional")
    }
  }
};
</script>

We have the isOptional field with the required property set to the requiredUnless validator.

requiredUnless(“isOptional”) means that the field is required when isOptional is true.

Conclusion

We can validate fields dynamically with Vuelidate.

It also comes with many validators built-in.

Categories
Vue

Vuelidate — Custom Validators

Vue.js doesn’t come with any form validation capabilities by default.

Therefore, we need to add our own form validation library or with our own code.

In this article, we’ll look at how to create custom validators with Vuelidate.

Custom Validators

We can create our own custom validators with Vuelidate.

For instance, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.required">name is required.</div>
      <div v-if="!$v.name.mustBeCool">name is invalid.</div>
    </div>
  </div>
</template>
<script>
import { required } from "vuelidate/lib/validators";
const mustBeCool = value => value.includes("cool");

export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      required,
      mustBeCool
    }
  }
};
</script>

We created the mustBeCool function to validate the name field with it.

value has the inputted value.

We just return what we expect to be valid in the function.

Optional Validator

We can also validate optional fields with our custom validator.

To do this, we use a helper function from Vuelidate.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.mustBeCool">name is invalid.</div>
    </div>
  </div>
</template>
<script>
import { helpers } from "vuelidate/lib/validators";
const mustBeCool = value => !helpers.req(value) || value.includes("cool");

export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      mustBeCool
    }
  }
};
</script>

to make name an optional field.

We only validate if the value is a non-empty string.

The helpers.req(value) to check if the value is required or not.

Negating that means we make the field optional.

Extra Parameters

We can add extra parameters to the field.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.mustBeCool">name is invalid.</div>
    </div>
  </div>
</template>
<script>
import { helpers } from "vuelidate/lib/validators";
const contains = param => value => !helpers.req(value) || value.includes(param);
export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      mustBeCool: contains("cool")
    }
  }
};
</script>

to add a parameter to our code for validation.

We created the contains function which takes a param parameter.

Then we can check for param with the includes method to see if what we entered is included.

value has the inputted value.

$props Support

Also, we can use the withParams method to create our validator with parameters.

If we use withParams , we get the information about the validation rule with the $params property.

For example, we can write:

<template>
  <div id="app">
    <div>
      <label>name</label>
      <input v-model.trim="$v.name.$model">
      <div v-if="!$v.name.mustBeCool">name is invalid.</div>
    </div>
  </div>
</template>
<script>
import { helpers } from "vuelidate/lib/validators";
const contains = param =>
  helpers.withParams(
    { type: "contains", value: param },
    value => !helpers.req(value) || value.includes(param)
  );

export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      mustBeCool: contains("cool")
    }
  }
};
</script>

We called the helpers.withParams method to get our parameter for our contains function.

The type property lets us set the type of property that we’re accepting.

Conclusion

We can create custom validators with Vuelidate.

Categories
Vue

Vuelidate — Async Validation and Rules Data

Vue.js doesn’t come with any form validation capabilities by default.

Therefore, we need to add our own form validation library or with our own code.

In this article, we’ll look at how to validate forms with Vuelidate.

Asynchronous Validation

We can validate form fields asynchronously.

For example, we can write:

<template>
  <div id="app">
    <div :class="{ 'form-group--error': $v.$error }">
      <label>Name</label>
      <input v-model.trim="$v.name.$model">
    </div>
    <div v-if="!$v.name.required">Name is required.</div>
    <div v-if="!$v.name.isUnique">Name already exists.</div>
  </div>
</template>
<script>
import { required } from "vuelidate/lib/validators";

export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      required,
      isUnique(value) {
        if (value === "") return true;
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve(typeof value === "string" && value.length % 2 !== 0);
          }, 350 + Math.random() * 300);
        });
      }
    }
  }
};
</script>

We have the isUnique method that returns a promise that resolves to true if value is string and its length is even.

value is the value that we inputted.

true means the value is invalid.

In the template, we just check the rule with the $v.name.isUnique property.

We can also use the async and await syntax for async validation.

We just return the resolved value inside the function:

validations: {
  async isUnique (value) {
    if (value === '') return true
    const response = await fetch(`/api/unique/${value}`)
    return Boolean(await response.json())
  }
}

Delayed Validation Errors

We can delay validation errors.

To do that, we can create a touch method that is delayed.

For example, we can write:

<template>
  <div id="app">
    <div :class="{ 'form-group--error': $v.$error }">
      <label>Name</label>
      <input v-model.trim="$v.name.$model" @input="delayTouch($v.name)">
    </div>
    <div v-if="!$v.name.required">Name is required.</div>
    <div
      class="error"
      v-if="!$v.name.minLength"
    >Name must have at least {{$v.name.$params.minLength.min}} letters.</div>
    <div
      class="error"
      v-if="!$v.name.maxLength"
    >Name must have at most {{$v.name.$params.maxLength.max}} letters.</div>
  </div>
</template>
<script>
import { required, minLength, maxLength } from "vuelidate/lib/validators";
const touchMap = new WeakMap();

export default {
  name: "App",
  data() {
    return {
      name: ""
    };
  },
  validations: {
    name: {
      required,
      minLength: minLength(4),
      maxLength: maxLength(15)
    }
  },
  methods: {
    delayTouch($v) {
      $v.$reset();
      if (touchMap.has($v)) {
        clearTimeout(touchMap.get($v));
      }
      touchMap.set($v, setTimeout($v.$touch, 1000));
    }
  }
};
</script>

to add a delayTouch method to run the form validation code with a delay.

We have a weak map to check store the timer that runs $v.$touch with a delay.

If the timer exists in the weak map, we clear the timeout.

This way, we won’t have to create a new timer every time delayTouch is run.

delayTouch is run when the input event is emitted.

Accessing Validator Parameters

We can access the $params property of the child field.

For example, we can write:

<template>
  <div id="app">
    <div :class="{ 'form-group--error': $v.$error }">
      <label>Name</label>
      <input v-model.trim="$v.form.name.$model">
    </div>
    <div v-if="!$v.form.name.required">Name is required.</div>
    <div
      v-if="!$v.form.name.minLength"
    >Name must have at least {{$v.form.name.$params.minLength.min}} letters.</div>
    <div
      v-if="!$v.form.name.maxLength"
    >Name must have at most {{$v.form.name.$params.maxLength.max}} letters.</div>
  </div>
</template>
<script>
import { required, minLength, maxLength } from "vuelidate/lib/validators";

export default {
  name: "App",
  data() {
    return {
      form: {
        name: ""
      }
    };
  },
  validations: {
    form: {
      name: {
        required,
        minLength: minLength(4),
        maxLength: maxLength(15)
      }
    }
  }
};
</script>

We have the $v.form.name.$params.minLength.min and $v.form.name.$params.maxLength.max properties to access the min and max number of allowed characters for the name field.

Conclusion

We can validate form fields asynchronously and access form validation rules data with Vuelidate.