Vue.js: Validation with TypeScript
Published on Tuesday, February 6, 2018 6:00:00 AM UTC in Programming
Whenever you work with forms and input fields, sooner or later validation of user input becomes an issue. Of course there's always the possibility to hand-code valdation into your app, but this is one of the topics that look straight-forward at first and then turn into a mess of trial and error quickly. When you're looking for a library to help you with that, you'll probably stumble upon the great curated list on vuejs.org. There's exactly one validation framework listed there, Vuelidate, which looks nice, easy to implement and is very actively maintained. When you try to integrate your TypeScript-based app with Vuelidate, you'll run into several issues that are not so easy to resolve though. One of the major pain points also is that typings are missing completely. After a lot of searching and testing different options, I found a better alternative: Vee-Validate. It's equally active maintained, has a very good documentation and has proven to be an excellent choice for TypeScript developers.
Installation of Vee-Validate
This is straight forward: simply add the package vee-validate
to your project using Yarn or NPM. All you have to do in addition is import it and configure Vue to actually make use of it:
// in your Main.ts
import Vue from "vue";
import VeeValidate from "vee-validate";
Vue.use(VeeValidate);
From there on, you can use Vee-Validate in all your components. It has a mostly declarative way of working with it, which keeps your component code clean and short. I won't go into the details of its features, because the original author is doing a much better job at that. What I want to do though is point you at some optimizations and potential problems you might run into, completely with solutions.
Share Validators Across Components
The default behavior of Vee-Validate is to inject a so-called validator into every component automatically. This serves simple scenarios well, but may be undesirable in more complex applications. The obvious thing happening is that for an app with many (small) components, a lot of unused validators are created. There's another reason you might want to turn that feature off though: to potentially share validators across components.
Let's say you have a screen in your app that allows editing some data. The more complex the data becomes, the more likely you want to split the involved UI and application logic into separate components. So now you have a container component and two, three, or potentially more sub-components that basically work on the same data. In Vue.js, this is easy to achieve and nicely falls into place with the concepts of passing "down" data to sub-components using bindings. The validation story though becomes more complicated by that: individual validations (say, required fields), are happening in the sub-components, but there are overall actions (think: save button) that are located in the container and still want to use the validation results of all the sub-components, for example to decide whether it's OK to perform some action.
To enable this scenario, you have to do the following:
- Turn off auto-injection of validators.
- Consciously decide where to create new validators, and
- Pass on parent validators to sub-components where necessary to share them
Sounds complicated, but it's easy.
// in your Main.ts
Vue.use(VeeValidate, { inject: false });
This turns off the auto-injection feature. From now on, to get a new validator for a component, you have to explicitly specify that in your @Component
decorator:
@Component({
components: { MySubComponent },
$_veeValidate: { validator: "new" }
})
export class MyComponent extends Vue {
}
Validators are "provided" automatically, meaning in sub-components you can have Vue.js inject them. Like this:
// in "MySubComponent"
@Inject('$validator') public $validator!: Validator;
Now, all the validations that you define in MySubComponent
automatically use the validator of the parent MyComponent
. This works for an arbitrary number of components. This enables you to do the following in MyComponent
:
public async save(): Promise<void> {
// collects validation results from sub-components too!
let allOK = await this.$validator.validateAll();
// ...
}
That's a very nice feature to aggregate validation results from sub-components without any manual interaction.
Workaround for Missing Typings
At the moment, the typings of Vee-Validate still contain an old (deprecated) syntax for explicitly creating new validators. I've created an issue for that. Until it's fixed, you have to manually add the following to your projects, for example in some central .d.ts
-file, to prevent TypeScript compilation errors:
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
// this is required because current typings of vee-validate have the old $validates in them, which doesn't work anymore
$_veeValidate?: any;
}
}
This is a temporary thing and can be removed once the package has been updated.
Fixing Potential Timing Issues
Depending on the UI framework you are using and the particular model you're binding to (seems to happen more often if you "dot" through to sub-properties), you might come across timing issues. These "one off" errors result in a slightly wrong behavior where the validation always seems to lag one step behind. Like this:
Typically, you would want to defer the validation by one "tick" to avoid this, i.e. use Vue.js's $nexttick
callback feature. But since all validation happens declaratively and inside Vee-Validate, this is not easily possible. But there's an easy fix by configuring Vee-Validate with a very small delay:
// in your Main.ts
// HACK: delay: 1 avoids "one-behind"-error in text fields with dot-properties
Vue.use(VeeValidate, { inject: false, delay: 1 });
Should you run into these issues, this has the same effect and will result in the desired behavior:
Much better :).
Have fun!
Side Rant
This is my personal opinion on Vuelidate with TypeScript, currently the only "semi-officially" endorsed validation framework.
It's always delicate to complain about open source projects. Very often people are dedicating their free time to these projects, and on the consuming side of things you get all the benefits without any efforts or payments. This means it's simply not appropriate to nag and whine when a basically free library doesn't do or fails to provide what you want. When I started using Vuelidate with TypeScript though, the background story of this relationship that I discovered made me cringe.
So, a year ago someone who also had trouble with the library and TypeScript submits a pull request providing typings, and also offers to actively maintain these typings going forward. The pull request gets rejected because the repo owner is not into TypeScript and wants tests to ensure everythings working, and that he doesn't accidentally break anything in the future - fair enough and completely legitimate. The author of the pull request seems even more motivated by that and comes back with a test suite. Four months(!) later the repo owner adds a short comment that he "has to wait for the right moment" to do the review and add the pull request to the project. Which never happens. Two more months later the original author closes the pull request and dumps it. There are more issues on GitHub talking about TypeScript integration, and again multiple devs try to kickstart adding typings to the project, providing proof of concepts or first versions over the course of months. Again, the repo owner gets excited. Two months ago, he once again states that he's going to "roll out the typings into the offical package soon". Until today, nothing happened though.
For me, stories like this are reason enough to walk away from a project, no matter how good it may be otherwise. Don't get me wrong. Like I said, there's no obligation for any open source author for anything, not even if you're listed on an officially maintained list of another, big project. And at the same time there's no entitlement by others to demand a particular feature to be added to a project (you can always fork, can't you). The thing here however is that people are directly or indirectly encouraged again and again to get involved, and are presented the prospect of getting their changes into the project, but nothing happens. It's OK to not have time. It's OK to not have interest in a particular language or feature. But it's not OK to make promises you cannot keep, and to disappoint potential contributors in ways like that.
Tags: TypeScript · Vue.js