Repository: baianat/vee-validate
Pull request #954 (2017-11-05 11:06:26)

Do not apply delay to blur event
As mentioned by @ssendev in #620 the validation delay does not make a lot of sense on blur. The primary objective of the delay would pretty much always be to stop error messages while typing. Since the blur event needs to be triggered by the user manually, there is no point in applying the delay there too.

Of course, the topic of #620 originally was more complex: The delay and events should be dynamic based on the status of the field. It makes a lot of sense, but it does require more changes.

Now I will admit, my solution is all but ideal, but it works (locally tested) and it does solve the issue. I do hope it can be improved further (config option, separate delay for blur events, less awful code).

Since the code quality isn't great and I couldn't figure out how to properly test specific event delays I don't think it should be merged necessarily. If anyone needs this feature in this hacky form, they can use my repo and ``npm link`` locally.

- [x] Disable delay on ``blur``
- [x] Make the behaviour of ``blur`` configurable
- [x] Allow configuration of delays on a per event basis
- [x] Write documentation


Patch:
diff --git a/docs/api.html b/docs/api.html
index e9e04b5d..73625e2e 100644
--- a/docs/api.html
+++ b/docs/api.html
@@ -53,7 +53,7 @@ const expression = {
         </tr>
         <tr>
             <td class="is-method-name">data-vv-delay</td>
-            <td>Specifies the delay amount in milliseconds for triggering the validation.</td>
+            <td>Specifies the delay amount in milliseconds for triggering the validation (input event only).</td>
         </tr>
         <tr>
             <td class="is-method-name">data-vv-name</td>
@@ -225,7 +225,7 @@ validator.validate('email', 'foo@bar'); // false
 <blockquote>
 <p>All validators share the same locale configuration. so any locale changes will update all validator instances across your app. For more information about how to overwrite messages and add new ones, please refer to the <a href="rules.html#custom-messages">custom messages</a> section.</p>
 </blockquote>
-<pre><code class="highlight-js">import { Validator } from 'vee-validate'; 
+<pre><code class="highlight-js">import { Validator } from 'vee-validate';
 
 // Also exposed on the class.
 Validator.setLocale('ar'); // Set all validator locales to 'ar'.
@@ -327,4 +327,4 @@ if (location.hash) {
   var hash = location.hash;
   location.hash = '';
   location.hash = hash;
-}</script><script type="text/javascript" src="/assets/js/vendor-a0decb2bff00e426a1fc.js"></script><script type="text/javascript" src="/assets/js/bundle-a0decb2bff00e426a1fc.js"></script></body></html>
\ No newline at end of file
+}</script><script type="text/javascript" src="/assets/js/vendor-a0decb2bff00e426a1fc.js"></script><script type="text/javascript" src="/assets/js/bundle-a0decb2bff00e426a1fc.js"></script></body></html>
diff --git a/docs/examples.html b/docs/examples.html
index 11280420..b2106604 100644
--- a/docs/examples.html
+++ b/docs/examples.html
@@ -6,7 +6,7 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
 ga('create', 'UA-100131478-1', 'auto');
 ga('send', 'pageview');
 </script><svg style="display: none;"><symbol id="github" viewbox="0 0 16 16"><path d="M8 0C3.58 0 0 3.582 0 8c0 3.535 2.292 6.533 5.47 7.59.4.075.547-.172.547-.385 0-.19-.007-.693-.01-1.36-2.226.483-2.695-1.073-2.695-1.073-.364-.924-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.223 1.873.87 2.33.665.072-.517.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.953 0-.873.31-1.587.823-2.147-.09-.202-.36-1.015.07-2.117 0 0 .67-.215 2.2.82.64-.178 1.32-.266 2-.27.68.004 1.36.092 2 .27 1.52-1.035 2.19-.82 2.19-.82.43 1.102.16 1.915.08 2.117.51.56.82 1.274.82 2.147 0 3.073-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38C13.71 14.53 16 11.53 16 8c0-4.418-3.582-8-8-8"></path></symbol><symbol id="i-star" viewbox="0 0 32 32"><path d="M16 2 L20 12 30 12 22 19 25 30 16 23 7 30 10 19 2 12 12 12 Z"></path></symbol><div class="columns is-mobile" id="app"><button :class="{ 'button': true, 'sidebar-button': true, 'is-open': sidebarToggle }" @click="sidebarToggle = ! sidebarToggle" type="button"><span class="icon"><i class="fa fa-bars"></i></span></button><div class="column is-2 sidebar-container" :class="{ 'column is-2 sidebar-container': true, 'is-open': sidebarToggle }" v-cloak><aside class="menu sidebar"><animated-button link="https://github.com/logaretm/vee-validate"><div class="flex-div" slot="default"><svg class="icon"><use xlink:href="#github"></use></svg>Star</div><div class="flex-div" slot="hover"><svg class="icon"><use xlink:href="#i-star"></use></svg>{{ stars }}</div></animated-button><p class="menu-label">Getting Started</p><ul class="menu-list"><li><a href="index.html#about">Introduction</a></li><li><a href="index.html#installation">Installation</a><ul><li><a href="index.html#npm">npm</a></li><li><a href="index.html#cdn">CDN</a></li></ul></li><li><a href="index.html#basic-example">Basic Example</a></li><li><a href="index.html#render-errors">Rendering Errors</a></li><li><a href="index.html#available-rules">Available Validations</a></li><li><a href="index.html#configuration">Configuration</a></li></ul><p class="menu-label">Validation Rules</p><ul class="menu-list"><li><a href="rules.html#syntax">Syntax</a></li><li><a href="rules.html#available-validations">Available Validations</a></li><li><a href="rules.html#custom-rules">Custom Validations</a></li><li><a href="rules.html#custom-messages">Custom Messages</a></li><li><a href="rules.html#custom-attributes">Custom Attributes</a></li><li><a href="rules.html#field-sepecific-messages">Field-specific Custom Messages  </a></li></ul><p class="menu-label">Localization</p><ul class="menu-list"><li><a href="localization.html#translation">Translation</a></li><li><a href="localization.html#attributes-data-as">Attributes</a><ul><li><a href="localization.html#attributes-data-as">alias</a></li><li><a href="localization.html#attributes-dictionary">dictionary</a></li></ul></li><li><a href="localization.html#localized-files">Localized Files</a></li></ul><p class="menu-label">Advanced</p><ul class="menu-list"><li><a href="advanced.html#inject">Component Injections</a></li></ul><p class="menu-label">Examples</p><ul class="menu-list"><li><a href="examples.html#debounce-example">Debounce</a></li><li><a href="examples.html#reject-example">Reject Files</a></li><li><a href="examples.html#validate-data-example">Validating Models</a></li><li><a href="examples.html#initial-value">Initial Value Validation</a></li><li><a href="examples.html#validate-form">Form Validation</a></li><li><a href="examples.html#locale-example">Localized Messages</a></li><li><a href="examples.html#scope-example">Validation Scopes</a></li><li><a href="examples.html#coupon-example">Custom Validation: Coupon</a></li><li><a href="examples.html#radio-buttons-example">Radio Buttons Validation</a></li><li><a href="examples.html#checkbox-example">Checkbox Example</a></li><li><a href="examples.html#flags-example">Validation Flags</a></li><li><a href="examples.html#selectors-example">Error Selectors</a></li><li><a href="examples.html#component-example">Component Validation</a></li><li><a href="examples.html#event-bus-example">Event Bus</a></li></ul><p class="menu-label">API</p><ul class="menu-list"><li><a href="api.html#directive">v-validate</a></li><li><a href="api.html#data-attributes">data-* Attributes</a></li><li><a href="api.html#error-bag">Error Bag</a></li><li><a href="api.html#validator">Validator</a><ul><li><a href="api.html#validator">API</a></li><li><a href="api.html#validator-example">Example</a></li></ul></li></ul></aside></div><div class="column columns is-mobile main-container" @click="sidebarToggle = false"><div class="column is-12"><section class="hero"><div class="hero-body"><div class="container has-text-centered"><img width="150" alt="VeeValidate Logo" src="https://s3.eu-central-1.amazonaws.com/logaretm/vee-validate.svg"><h1 class="title is-2">Examples </h1><h2 class="subtitle is-4">Usage and Examples</h2></div></div></section><div class="container"><div class="column content-container is-mobile"><h2><a href="#debounce-example">Delayed Validation (Debounced)</a></h2>
-<p>You can specify a delay to debounce the input event, a case scenario that you may want to wait for the user to stop typing then validate the field. This can be achieved by adding a <code>data-vv-delay</code> attribute on the field being validated, and assign it the number of milliseconds you want to wait for.</p>
+<p>You can specify a delay to debounce the input event, a case scenario that you may want to wait for the user to stop typing then validate the field. This can be achieved by adding a <code>data-vv-delay</code> attribute on the field being validated, and assign it the number of milliseconds you want to wait for. (Notice: This directive only affects the input event and will override global configuarion for the input event)</p>
 <code-example><div slot="example"><delay-example></delay-example></div><div slot="js">export default {
   name: 'delay-example'
 };
@@ -488,15 +488,15 @@ export default {
 }
 </code></pre>
 <p>The global fields flags are accessed via objects like this:</p>
-<pre><code class="highlight-js">// Is the 'name' field dirty? 
+<pre><code class="highlight-js">// Is the 'name' field dirty?
 this.fields.name.dirty;
 </code></pre>
 <p>However, for the scoped fields the <strong>FieldBag</strong> will group those fields in an property name that is prefixed by a <code>$</code> to indicate that it is a scope object:</p>
-<pre><code class="highlight-js">// Is the 'name' field dirty? 
+<pre><code class="highlight-js">// Is the 'name' field dirty?
 this.fields.$myScope.name.dirty;
 
-// Is the 'name' field clean? 
-this.fields.$myScope.name.pristine; 
+// Is the 'name' field clean?
+this.fields.$myScope.name.pristine;
 </code></pre>
 <p>Here is what it would look like:</p>
 <pre><code class="highlight-html">&lt;div class=&quot;form-input&quot;&gt;
@@ -564,10 +564,10 @@ this.$validator.flag('scoped.field', {
 </code></pre>
 <p>For custom components, in order for the flags to fully work reliably, you need to emit those events:</p>
 <p>The input event, which you probably already emit, will set the dirty and pristine flags.</p>
-<pre><code class="highlight-js">this.$emit('input', value); 
+<pre><code class="highlight-js">this.$emit('input', value);
 
 // The focus event which will set the touched and untouched flags.
-this.$emit('focus'); 
+this.$emit('focus');
 </code></pre>
 <p>Here is an example that displays those flags, intereact with the input and watch the flags change accordingly:</p>
 <code-example><div slot="example"><flags-example></flags-example></div><div slot="js">import { mapFields } from 'vee-validate';
@@ -590,7 +590,7 @@ export default {
         &lt;p class=&quot;control has-icon has-icon-right&quot;&gt;
             &lt;input name=&quot;name&quot; v-validate=&quot;'required|alpha'&quot; :class=&quot;{'input': true }&quot; type=&quot;text&quot; placeholder=&quot;Name&quot;&gt;
         &lt;/p&gt;
-        &lt;pre&gt;{{&quot;{&quot; + &quot;{ nameFlags }&quot; + &quot;}&quot;}}&lt;/pre&gt;            
+        &lt;pre&gt;{{&quot;{&quot; + &quot;{ nameFlags }&quot; + &quot;}&quot;}}&lt;/pre&gt;
     &lt;/div&gt;
 &lt;/div&gt;
 </div></code-example><h2><a href="#selectors-example">Error Selectors</a></h2>
@@ -814,4 +814,4 @@ if (location.hash) {
   var hash = location.hash;
   location.hash = '';
   location.hash = hash;
-}</script><script type="text/javascript" src="/assets/js/vendor-a0decb2bff00e426a1fc.js"></script><script type="text/javascript" src="/assets/js/bundle-a0decb2bff00e426a1fc.js"></script></body></html>
\ No newline at end of file
+}</script><script type="text/javascript" src="/assets/js/vendor-a0decb2bff00e426a1fc.js"></script><script type="text/javascript" src="/assets/js/bundle-a0decb2bff00e426a1fc.js"></script></body></html>
diff --git a/docs/index.html b/docs/index.html
index 309b413a..12e7998c 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -23,7 +23,7 @@ ga('send', 'pageview');
 <pre><code class="highlight-html">  &lt;script src=&quot;path/to/vue.js&quot;&gt;&lt;/script&gt;
   &lt;script src=&quot;path/to/vee-validate.js&quot;&gt;&lt;/script&gt;
   &lt;script&gt;
-    Vue.use(VeeValidate); // good to go. 
+    Vue.use(VeeValidate); // good to go.
   &lt;/script&gt;
 </code></pre>
 <p>or you may import it using <strong>ES6</strong>:</p>
@@ -63,7 +63,7 @@ Vue.use(VeeValidate);
 <h2><a href="#available-rules">Available Rules</a></h2>
 <p>There are more than 20 rules available to validate your inputs:</p>
 <div class="columns is-multiline">
-  <div class="column is-4"> 
+  <div class="column is-4">
 <ul>
 <li>
 <p><a href="rules.html#rule-after">after</a></p>
@@ -173,12 +173,12 @@ import VeeValidate from 'vee-validate';
 
 const config = {
   errorBagName: 'errors', // change if property conflicts
-  fieldsBagName: 'fields', 
-  delay: 0, 
-  locale: 'en', 
-  dictionary: null, 
-  strict: true, 
-  classes: false, 
+  fieldsBagName: 'fields',
+  delay: 0,
+  locale: 'en',
+  dictionary: null,
+  strict: true,
+  classes: false,
   classNames: {
     touched: 'touched', // the control has been blurred
     untouched: 'untouched', // the control hasn't been blurred
@@ -211,13 +211,13 @@ Vue.use(VeeValidate, config);
         </tr>
         <tr>
             <td class="is-method-name">fieldsBagName</td>
-            <td>fields</td>            
+            <td>fields</td>
             <td>The name of the Fields (flags) object that will be injected in each of Vue's instances' data.</td>
         </tr>
         <tr>
             <td class="is-method-name">delay</td>
             <td>0</td>
-            <td>The default debounce time for all inputs (only affects validations).</td>
+            <td>The default debounce time for all inputs (only affects validations). This can optionally contain an object in the form of { event: delay, ... } where delays can be specified on a per event basis.</td>
         </tr>
         <tr>
             <td class="is-method-name">locale</td>
@@ -227,7 +227,7 @@ Vue.use(VeeValidate, config);
         <tr>
             <td class="is-method-name">dictionary</td>
             <td>null</td>
-            <td>  
+            <td>
               A dictionary to be merged with the validators dictionary. (Check the [custom messages](rules.html#custom-messages) and [localization](localization.html) sections.)
             </td>
         </tr>
@@ -305,4 +305,4 @@ if (location.hash) {
   var hash = location.hash;
   location.hash = '';
   location.hash = hash;
-}</script><script type="text/javascript" src="/assets/js/vendor-a0decb2bff00e426a1fc.js"></script><script type="text/javascript" src="/assets/js/bundle-a0decb2bff00e426a1fc.js"></script></body></html>
\ No newline at end of file
+}</script><script type="text/javascript" src="/assets/js/vendor-a0decb2bff00e426a1fc.js"></script><script type="text/javascript" src="/assets/js/bundle-a0decb2bff00e426a1fc.js"></script></body></html>
diff --git a/locale/sv.js b/locale/sv.js
index 6f850ef0..edede83a 100644
--- a/locale/sv.js
+++ b/locale/sv.js
@@ -2,8 +2,8 @@ import { formatFileSize, isDefinedGlobally } from './utils';
 
 const messages = {
   after: (field, [target]) => `Fältet ${field} måste vara efter ${target}.`,
-  alpha_dash: (field) => `Fältet ${field} får bara innehålla alfanumeriska tecken såväl som snedstreck och understreck.`,
-  alpha_num: (field) => `Fältet ${field} får bara innehålla alfanumeriska tecken.`,
+  alpha_dash: (field) => `Fältet ${field} får bara innehålla alfa-numeriska tecken såväl som snedstreck och understreck.`,
+  alpha_num: (field) => `Fältet ${field} får bara innehålla alfa-numeriska tecken.`,
   alpha_spaces: (field) => `Fältet ${field} får bara innehålla alfabetiska tecken och mellanslag.`,
   alpha: (field) => `Fältet ${field} får bara innehålla alfabetiska tecken.`,
   before: (field, [target]) => `Fältet ${field} måste vara tidigare än ${target}.`,
@@ -13,19 +13,19 @@ const messages = {
   date_format: (field, [target]) => `Fältet ${field} måste ha formatatet ${target}.`,
   decimal: (field, [decimals = '*'] = []) => `Fältet ${field} måste vara numeriskt och får innehålla ${(decimals === '*' ? '' : decimals)} decimaltecken.`,
   digits: (field, [length]) => `Fältet ${field} måste vara numeriskt och innehålla exakt ${length} siffor.`,
-  dimensions: (field, [width, height]) => `Fältet ${field} måste vara ${width} pixlar bred och ${height} pixlar hög.`,
+  dimensions: (field, [width, height]) => `Fältet ${field} måtste vara ${width} pixlar bred och ${height} pixlar hög.`,
   email: (field) => `Fältet ${field} måste vara en giltig e-postadress.`,
   ext: (field) => `Fältet ${field} måste vara en godkänd fil.`,
   image: (field) => `Fältet ${field} måste vara en bildfil.`,
-  in: (field) => `Fältet ${field} måste vara ett godkänt alternativ.`,
+  in: (field) => `Fältet ${field} måste vara ett godkänt alternativ`,
   ip: (field) => `Fältet ${field} måste vara en godkänd ip-adress.`,
   max: (field, [max]) => `Fältet ${field} får inte vara längre än ${max} tecken.`,
   mimes: (field) => `Fältet ${field} måste ha en filändelse.`,
   min: (field, [min]) => `Fältet ${field} måste minst vara ${min} tecken.`,
-  not_in: (field) => `Fältet ${field} måste vara ett godkänt alternativ.`,
+  not_in: (field) => `Fältet ${field} måste vara ett godkänt alternativ `,
   numeric: (field) => `Fältet ${field} får bara innehålla numeriska tecken.`,
   regex: (field) => `Fältet ${field} har en felaktig formatering.`,
-  required: (field) => `Fältet ${field} är obligatoriskt.`,
+  required: (field) => `Fältet ${field} är obligatoriskt`,
   size: (field, [size]) => `Fältet ${field} måste vara mindre än ${formatFileSize(size)}.`,
   url: (field) => `Fältet ${field} är inte en godkänd URL.`
 };
diff --git a/src/core/field.js b/src/core/field.js
index 93c8c248..36cd5f6d 100644
--- a/src/core/field.js
+++ b/src/core/field.js
@@ -1,4 +1,20 @@
-import { uniqId, createFlags, assign, normalizeRules, isNullOrUndefined, setDataAttribute, toggleClass, getInputEventName, debounce, isCallable, warn, toArray, getPath } from './utils';
+import {
+  uniqId,
+  createFlags,
+  assign,
+  normalizeRules,
+  isNullOrUndefined,
+  setDataAttribute,
+  toggleClass,
+  getInputEventName,
+  debounce,
+  isCallable,
+  warn,
+  toArray,
+  getPath,
+  makeEventsArray,
+  makeDelayObject,
+} from './utils';
 import Generator from './generator';
 
 // @flow
@@ -50,7 +66,7 @@ export default class Field {
   initial: boolean;
   classes: boolean;
   classNames: { [string]: string };
-  delay: number;
+  delay: object;
   listen: boolean;
   model: ?string;
 
@@ -61,6 +77,7 @@ export default class Field {
     this.dependencies = [];
     this.watchers = [];
     this.events = [];
+    this.delay = 0;
     this.rules = {};
     if (!this.isHeadless && !options.targetOf) {
       setDataAttribute(this.el, 'id', this.id); // cache field id if it is independent and has a root element.
@@ -181,8 +198,8 @@ export default class Field {
     this.classNames = options.classNames || this.classNames || DEFAULT_OPTIONS.classNames;
     this.alias = options.alias || this.alias;
     this.getter = isCallable(options.getter) ? options.getter : this.getter;
-    this.delay = options.delay || this.delay || 0;
-    this.events = typeof options.events === 'string' && options.events.length ? options.events.split('|') : this.events;
+    this.events = (options.events) ? makeEventsArray(options.events) : this.events;
+    this.delay = (options.delay) ? makeDelayObject(this.events, options.delay) : makeDelayObject(this.events, this.delay);
     this.updateDependencies();
     this.addActionListeners();
 
@@ -448,7 +465,6 @@ export default class Field {
       this.validator.validate(`#${this.id}`, args[0]);
     };
 
-    const validate = debounce(fn, this.delay);
     const inputEvent = getInputEventName(this.el);
     // replace input event with suitable one.
     let events = this.events.map(e => {
@@ -457,7 +473,7 @@ export default class Field {
 
     // if there is a watchable model and an on input validation is requested.
     if (this.model && events.indexOf(inputEvent) !== -1) {
-      const unwatch = this.vm.$watch(this.model, validate);
+      const unwatch = this.vm.$watch(this.model, this.delay[inputEvent]);
       this.watchers.push({
         tag: 'input_model',
         unwatch
@@ -468,6 +484,8 @@ export default class Field {
 
     // Add events.
     events.forEach(e => {
+      let validate = debounce(fn, this.delay[e]);
+
       if (this.isVue) {
         this.component.$on(e, validate);
         this.watchers.push({
diff --git a/src/core/generator.js b/src/core/generator.js
index 77210c5c..2545a2c9 100644
--- a/src/core/generator.js
+++ b/src/core/generator.js
@@ -8,7 +8,8 @@ import {
   getPath,
   hasPath,
   isNullOrUndefined,
-  isCallable
+  isCallable,
+  deepParseInt,
 } from './utils';
 
 /**
@@ -107,8 +108,15 @@ export default class Generator {
    * @param {*} vnode
    * @param {Object} options
    */
-  static resolveDelay (el, vnode, options = {}) {
-    return getDataAttribute(el, 'delay') || (vnode.child && vnode.child.$attrs && vnode.child.$attrs['data-vv-delay']) || options.delay;
+  static resolveDelay (el, vnode, options) {
+    let delay = getDataAttribute(el, 'delay');
+    let globalDelay = (options && 'delay' in options) ? options.delay : 0;
+
+    if (!delay && vnode.child && vnode.child.$attrs) {
+      delay = vnode.child.$attrs['data-vv-delay'];
+    }
+
+    return (delay) ? { local: { input: parseInt(delay) }, global: deepParseInt(globalDelay) } : { global: deepParseInt(globalDelay) };
   }
 
   /**
diff --git a/src/core/utils/index.js b/src/core/utils/index.js
index af4a0d62..0fe72200 100644
--- a/src/core/utils/index.js
+++ b/src/core/utils/index.js
@@ -378,5 +378,34 @@ export const isBuiltInComponent = (vnode): boolean => {
   const tag = vnode.componentOptions.tag;
 
   return /keep-alive|transition|transition-group/.test(tag);
-}
-;
+};
+
+export const makeEventsArray = (events: string) => {
+  return (typeof events === 'string' && events.length) ? events.split('|') : [];
+};
+
+export const makeDelayObject = (events: string[], delay: object | number) => {
+  let delayObject = {};
+
+  // We already have a valid delay object
+  if (typeof delay === 'object' && !('global' in delay) && !('local' in delay) && Object.keys(delay).length) return delay;
+
+  let globalDelay = (typeof delay === 'object' && 'global' in delay) ? delay.global : delay || 0;
+  let localDelay = (typeof delay === 'object' && 'local' in delay) ? delay.local : {};
+
+  events.forEach(e => {
+    delayObject[e] = (typeof globalDelay === 'object') ? localDelay[e] || globalDelay[e] || 0 : localDelay[e] || globalDelay;
+  });
+
+  return delayObject;
+};
+
+export const deepParseInt = (input: object | string) => {
+  if (typeof input === 'string') return parseInt(input);
+
+  for (const element in input) {
+    input[element] = parseInt(input[element]);
+  }
+
+  return input;
+};


Test patch:
diff --git a/tests/field.js b/tests/field.js
index a334c4cd..fd558574 100644
--- a/tests/field.js
+++ b/tests/field.js
@@ -1,581 +1,581 @@
-import Field from '../src/core/field';
-
-test('constructs a headless field with default values', () => {
-  const field = new Field(null, {});
-
-  expect(field.id).toBeTruthy(); // has been id'ed.
-  expect(field.scope).toBe(null);
-  expect(field.name).toBe(null);
-  expect(field.classes).toBe(false);
-  expect(field.rules).toEqual({});
-  expect(field.events).toEqual(['input', 'blur']);
-  expect(field.watchers.length).toBe(0);
-  expect(field.delay).toBe(0);
-  expect(field.flags).toEqual({
-    untouched: true,
-    touched: false,
-    dirty: false,
-    pristine: true,
-    valid: null,
-    invalid: null,
-    validated: false,
-    pending: false,
-    required: false
-  });
-  expect(field.classNames).toEqual({
-    touched: 'touched',
-    untouched: 'untouched',
-    valid: 'valid',
-    invalid: 'invalid',
-    pristine: 'pristine',
-    dirty: 'dirty'
-  });
-  expect(field.isHeadless).toBe(true);
-  field.classNames = null; // make sure it resets to the default value.
-  field.update({});
-  expect(field.classNames).toEqual({
-    touched: 'touched',
-    untouched: 'untouched',
-    valid: 'valid',
-    invalid: 'invalid',
-    pristine: 'pristine',
-    dirty: 'dirty'
-  });
-});
-
-test('fields can be configured to not add any value listener', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el, { listen: false });
-  expect(field.watchers.length).toBe(2); // just action listeners.
-});
-
-test('caches the field id on the element data attributes', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el);
-
-  // check if id was cached.
-  expect(el.getAttribute('data-vv-id')).toBe(field.id);
-  expect(field.isHeadless).toBe(false);
-});
-
-test('it adds value listeners on the native inputs', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el, { getter: () => el.value });
-
-  // two events [input and blur] along with same events for actions.
-  expect(field.watchers.length).toBe(4);
-  field.addValueListeners(); // Idempotence call.
-  expect(field.watchers.length).toBe(4); // still 4
-
-  field.vm = { $validator: { validate: jest.fn() } };
-
-  // test input event.
-  el.dispatchEvent(new Event('input'));
-  expect(field.validator.validate).toHaveBeenCalledWith(`#${field.id}`, field.value);
-
-  // blur event.
-  el.dispatchEvent(new Event('blur'));
-  expect(field.validator.validate).toHaveBeenCalledTimes(2);
-  expect(field.validator.validate).toHaveBeenLastCalledWith(`#${field.id}`, field.value);
-
-  // test event removal
-  field.unwatch(/input/);
-  // none are left.
-  expect(field.watchers.length).toBe(0);
-  el.dispatchEvent(new Event('blur'));
-
-  // has not changed since it was removed.
-  expect(field.validator.validate).toHaveBeenCalledTimes(2);
-});
-
-test('it adds value listeners on all radio inputs that have the same name', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="1" type="radio">
-    <input name="name" value="2" type="radio">
-    <input name="name" id="last" value="3" type="radio">
-  `;
-  const el = document.querySelector('#name');
-  const el2 = document.querySelector('#last');
-  const field = new Field(el, { getter: () => el.value });
-  field.vm = { $validator: { validate: jest.fn() } };
-  el2.dispatchEvent(new Event('change'));
-
-  expect(field.validator.validate).toHaveBeenCalledTimes(1); // triggered.
-  expect(field.watchers.length).toBe(8); // 4 for each field.
-  field.unwatch();
-  el2.dispatchEvent(new Event('change'));
-  expect(field.watchers.length).toBe(0);
-  expect(field.validator.validate).toHaveBeenCalledTimes(1);
-});
-
-test('it adds value listeners on the components', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el, {
-    component: { $on: jest.fn(), $off: jest.fn() },
-    getters: {
-      context: () => el,
-      value: (ctx) => ctx.value
-    }
-  });
-
-  // two events [input and blur] with the classes.
-  expect(field.watchers.length).toBe(4);
-  expect(field.component.$on).toHaveBeenCalledTimes(2);
-  field.addValueListeners(); // Idempotence call.
-  expect(field.watchers.length).toBe(4); // still 4
-});
-
-
-test('computes the disabled property', () => {
-  document.body.innerHTML = `<input name="name" id="name" value="10">`;
-  const el = document.querySelector('#name');
-  const field = new Field(el);
-
-  expect(field.isDisabled).toBe(false);
-  el.disabled = true;
-  expect(field.isDisabled).toBe(true);
-
-  field.component = { disabled: true };
-  expect(field.isDisabled).toBe(true);
-});
-
-test('computes the display name', () => {
-  const field = new Field(null, { name: 'email' });
-  // no alias was defined.
-  expect(field.displayName).toBe(undefined);
-  field.alias = 'aliased';
-  expect(field.displayName).toBe('aliased');
-});
-
-test('it exposes a boolean to check wether it is a component or not', () => {
-  let field = new Field(null, { component: null });
-  expect(field.isVue).toBe(false);
-  field = new Field(null, { component: {} });
-  expect(field.isVue).toBe(true);
-});
-
-test('it adds class listeners on the input', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10" type="text">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el, { classes: false });
-
-  // input, blur, focus, and another input.
-  expect(field.watchers.length).toBe(4);
-  field.addActionListeners(); // Idempotence call.
-  expect(field.watchers.length).toBe(4); // still 4
-
-  // if field classes are disabled do not add.
-  field.updateClasses();
-  expect(el.classList.contains('untouched')).toBe(false);
-  expect(el.classList.contains('pristine')).toBe(false);
-
-  field.update({ classes: true });
-  expect(el.classList.contains('untouched')).toBe(true);
-  expect(el.classList.contains('pristine')).toBe(true);
-
-
-  // test classes and one shot behavior.
-  el.dispatchEvent(new Event('input'));
-  expect(field.watchers.length).toBe(3); // 1 event down.
-  expect(el.classList.contains('dirty')).toBe(true);
-  expect(field.flags.dirty).toBe(true);
-  expect(el.classList.contains('pristine')).toBe(false);
-  expect(field.flags.pristine).toBe(false);
-
-  el.dispatchEvent(new Event('blur'));
-  expect(field.watchers.length).toBe(2); // 1 more down.
-  expect(el.classList.contains('untouched')).toBe(false);
-  expect(field.flags.untouched).toBe(false);
-  expect(el.classList.contains('touched')).toBe(true);
-  expect(field.flags.touched).toBe(true);
-
-  // disable the classes after being enabled.
-  field.addActionListeners(); // Idempotence call.
-  expect(field.watchers.length).toBe(4); // back to 4.
-  field.update({ classes: false }); // disable classes.
-  expect(field.watchers.length).toBe(4); // they remain because of flags.
-});
-
-test('calls validate if it was already validated and if there is a change in rules', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10" type="text">
-  `;
-  const el = document.querySelector('#name');
-  const vm = { $validator: { validate: jest.fn() } };
-  const field = new Field(el, { rules: 'required', vm });
-
-  // field is updated but hadn't been validated yet.
-  field.update({});
-  expect(vm.$validator.validate).toHaveBeenCalledTimes(0);
-
-  // validated but no change in rules.
-  field.flags.validated = true;
-  field.update({});
-  expect(vm.$validator.validate).toHaveBeenCalledTimes(0);
-
-  // change in rules.
-  field.update({ rules: 'required|alpha' });
-  expect(vm.$validator.validate).toHaveBeenCalledWith(`#${field.id}`);
-});
-
-test('it adds class listeners on components', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10" type="text">
-  `;
-  const component = {
-    evts: [],
-    $once: jest.fn((evt, cb) => {
-      component.evts.push({ evt, cb });
-    }),
-    $off: jest.fn((evt) => {
-      component.evts = component.evts.filter(e => e.evt !== evt);
-    }),
-    $on: jest.fn(),
-    $emit: jest.fn(event => {
-      component.evts.forEach(e => e.cb());
-    })
-  };
-  const el = document.querySelector('#name');
-  const field = new Field(el, {
-    events: '',
-    classes: true,
-    component,
-  });
-
-  expect(field.component.$once).toHaveBeenCalledTimes(2);
-  component.$emit('input');
-  // first call was to off the input listener.
-  expect(field.component.$off.mock.calls[0][0]).toBe('input');
-
-  component.$emit('blur');
-  // second call was to off the blur listener.
-  expect(field.component.$off.mock.calls[1][0]).toBe('blur');
-  expect(field.component.$off).toHaveBeenCalledTimes(2);
-
-  // only once.
-  component.$emit('input');
-  component.$emit('blur');
-  expect(field.component.$off).toHaveBeenCalledTimes(2);
-
-  // no events.
-  expect(field.watchers.length).toBe(0);
-});
-
-test('it checks against a specified matcher', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10" type="text">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el, { name: 'name' });
-  // match by name.
-  expect(field.matches({
-    name: 'name'
-  })).toBe(true);
-
-  // match by scope.
-   expect(field.matches({
-    scope: null
-  })).toBe(true);
-
-  // both
-  expect(field.matches({
-    scope: null,
-    name: 'email'
-  })).toBe(false);
-
-  expect(field.matches({
-    scope: null,
-    name: 'name'
-  })).toBe(true);
-
-  // match by id.
-  expect(field.matches({
-    id: field.id
-  })).toBe(true);
-
-  // no matcher.
-  expect(field.matches({})).toBe(true);
-});
-
-test('uses the watch API instead of adding listeners if the field is bound with a model', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10" type="text">
-  `;
-  const el = document.querySelector('#name');
-  // opt in for only the input event.
-  const field = new Field(el, { model: 'email', events: 'input', vm: { $watch: jest.fn() } });
-
-  // one for model, two for user actions.
-  expect(field.watchers.length).toBe(3);
-  expect(field.watchers[2].tag).toBe('input_model');
-  expect(field.vm.$watch).toHaveBeenCalled();
-});
-
-test('fields can be destroyed and have all their listeners cleaned up along with their dependencies', () => {
-  document.body.innerHTML = `
-    <input name="name" id="name" value="10" type="text">
-  `;
-  const el = document.querySelector('#name');
-  const field = new Field(el, { name: 'name', classes: true });
-  const destroy = jest.fn();
-  field.dependencies.push({ field: { destroy }});
-  expect(field.watchers.length).toBe(4);
-  field.destroy();
-  expect(field.watchers.length).toBe(0);
-  expect(destroy).toHaveBeenCalled();
-})
-
-test('sets aria attributes on elements', async () => {
-  let el = document.createElement('input');
-  const field = new Field(el, { rules: 'required' });
-  expect(el.getAttribute('aria-required')).toBe('true');
-  expect(el.getAttribute('aria-invalid')).toBe('false');
-
-  field.flags.invalid = true;
-  field.update({ rules: 'min:3' });
-  expect(el.getAttribute('aria-required')).toBe('false');
-  expect(el.getAttribute('aria-invalid')).toBe('true');
-  field.el = null; // headless
-  field.updateAriaAttrs();
-
-  // unchanged.
-  expect(el.getAttribute('aria-required')).toBe('false');
-  expect(el.getAttribute('aria-invalid')).toBe('true');
-});
-
-test('computes the rejectsFalse property', () => {
-  let el = document.createElement('input');
-  el.type = 'checkbox';
-  const vm = {
-    $validator: {
-      errors: {
-        update: jest.fn()
-      }
-    }
-  };
-  const component = {
-    value: 10,
-    events: {},
-    $el: document.body.children[0],
-    $watch: () => {},
-    $on: jest.fn()
-  };
-  let field = new Field(null);
-  expect(field.rejectsFalse).toBe(false);
-  field = new Field(null, {
-    component,
-    vm
-  });
-  expect(field.rejectsFalse).toBe(false);
-  field = new Field(el, { rules: 'required', vm });
-  expect(field.rejectsFalse).toBe(true);
-});
-
-test('calls the update method on the validator errors when updating scope', () => {
-  let el = document.createElement('input');
-  const vm = {
-    $validator: {
-      update: jest.fn()
-    }
-  };
-  const field = new Field(el, { rules: 'required', vm });
-  field.updated = true;
-  field.update({ rules: 'min:3', scope: 'scope' });
-  expect(vm.$validator.update).toHaveBeenCalledTimes(1);
-});
-
-test('calls the update method on the validator errors when updating scope to 0', () => {
-  let el = document.createElement('input');
-  const vm = {
-    $validator: {
-      update: jest.fn()
-    }
-  };
-  const field = new Field(el, { rules: 'required', vm });
-  field.updated = true;
-  field.update({ rules: 'min:3', scope: 0 });
-  expect(vm.$validator.update).toHaveBeenCalledTimes(1);
-});
-
-describe('fields can track their dependencies', () => {
-  test('native input depencies', () => {
-    document.body.innerHTML = `
-      <input name="other" id="name" value="10" type="text">
-    `;
-    let field = new Field(null, {
-      rules: 'required|confirmed:other',
-      vm: {
-        $el: document.body,
-        $validator: { validate: jest.fn() }
-      }
-    });
-    expect(field.dependencies.length).toBe(1);
-    expect(field.dependencies[0].name).toBe('confirmed');
-    expect(field.dependencies[0].field.value).toBe('10');
-    const destroy = jest.fn();
-    field.dependencies[0].field.destroy = destroy;
-    field.updateDependencies(); // test destroy.
-
-    expect(destroy).toHaveBeenCalled();
-  });
-
-  test('component dependencies', () => {
-    const component = {
-      value: 10,
-      $el: document.body.children[0],
-      $watch: () => {},
-      $on: () => {}
-    };
-
-    const field = new Field(null, {
-      rules: 'required|confirmed:$other',
-      vm: {
-        $el: document.body,
-        $refs: {
-          other: component
-        }
-      }
-    });
-
-    expect(field.dependencies.length).toBe(1);
-    expect(field.dependencies[0].name).toBe('confirmed');
-    expect(field.dependencies[0].field.value).toBe(10);
-  });
-
-  test('skips if no dependency was resolved', () => {
-    document.body.innerHTML = ``;
-    global.console.warn = jest.fn();
-    let field = new Field(null, {
-      rules: 'required|confirmed:other',
-      vm: {
-        $el: document.body
-      }
-    });
-
-    expect(field.dependencies.length).toBe(0);
-  });
-
-  test('validation triggers on the parent/controller field', () => {
-    document.body.innerHTML = `
-      <input name="other" id="name" value="10" type="text">
-    `;
-    const el = document.querySelector('#name');
-    const field = new Field(null, {
-      rules: 'required|confirmed:other',
-      vm: {
-        $el: document.body,
-        $validator: { validate: jest.fn() }
-      }
-    });
-
-    el.dispatchEvent(new Event('input'));
-    expect(field.dependencies[0].field.validator.validate).toHaveBeenCalledWith(`#${field.id}`);
-  });
-
-  test('fails silently with invalid selectors', () => {
-    document.body.innerHTML = `
-      <input name="other" id="name" value="10" type="text">
-    `;
-    let field = new Field(null, {
-      rules: 'required|confirmed:123-123-123',
-      vm: {
-        $el: document.body,
-        $validator: { validate: jest.fn() }
-      }
-    });
-    expect(field.dependencies.length).toBe(0);
-  });
-});
-
-test('set field custom validity if possible', () => {
-  document.body.innerHTML = `
-      <input name="other" id="name" value="10" type="text">
-    `;
-  const el = document.querySelector('#name');
-  const field = new Field(null, {
-    rules: 'required|confirmed:other',
-    vm: {
-      $el: document.body,
-      $validator: { validate: jest.fn(), errors: { firstById: () => 'Error' } }
-    }
-  });
-
-  el.setCustomValidity = jest.fn();
-  field.flags.valid = false;
-  field.flags.invalid = true;
-
-  field.updateCustomValidity();
-  expect(el.setCustomValidity).toHaveBeenCalledTimes(0);
-
-  field.el = el;
-  field.updateCustomValidity();
-  expect(el.setCustomValidity).toHaveBeenCalledTimes(1);
-
-  field.validator.errors.firstById = () => null;
-  // check unset.
-  field.flags.valid = true;
-  field.updateCustomValidity();
-  expect(el.setCustomValidity).toHaveBeenLastCalledWith('');
-
-  // Check set with no errors in error bag.
-  field.flags.valid = false;
-  field.updateCustomValidity();
-  expect(el.setCustomValidity).toHaveBeenLastCalledWith('');
-});
-
-test('resets field state', () => {
-  const el = document.createElement('input');
-  el.type = 'text';
-  el.name = 'field';
-  const vm = {
-    $validator: {
-      errors: {
-        removeById: jest.fn()
-      }
-    }
-  };
-  const field = new Field(el, { vm });
-  field.flags.valid = true;
-  field.el.setAttribute('aria-invalid', 'true');
-
-  field.reset();
-  expect(field.flags.valid).toBe(null);
-  expect(field.el.getAttribute('aria-invalid')).toBe('false');
-});
-
-test('sets the field flags', () => {
-  const field = new Field(null);
-  field.setFlags({
-    valid: false
-  });
-  expect(field.flags.invalid).toBe(true);
-  expect(field.flags.valid).toBe(false);
-
-  // specifiying the negated flag is respected.
-  field.setFlags({
-    touched: false,
-    untouched: false
-  });
-
-  expect(field.flags.touched).toBe(false);
-  expect(field.flags.untouched).toBe(false);
-});
-
-test('field scope set to 0', () => {
-  const field = new Field(null, {scope:0});
-  expect(field.scope).toBe(0);
-});
+import Field from '../src/core/field';
+
+test('constructs a headless field with default values', () => {
+  const field = new Field(null, {});
+
+  expect(field.id).toBeTruthy(); // has been id'ed.
+  expect(field.scope).toBe(null);
+  expect(field.name).toBe(null);
+  expect(field.classes).toBe(false);
+  expect(field.rules).toEqual({});
+  expect(field.events).toEqual(['input', 'blur']);
+  expect(field.watchers.length).toBe(0);
+  expect(field.delay).toEqual({'input': 0, 'blur': 0});
+  expect(field.flags).toEqual({
+    untouched: true,
+    touched: false,
+    dirty: false,
+    pristine: true,
+    valid: null,
+    invalid: null,
+    validated: false,
+    pending: false,
+    required: false
+  });
+  expect(field.classNames).toEqual({
+    touched: 'touched',
+    untouched: 'untouched',
+    valid: 'valid',
+    invalid: 'invalid',
+    pristine: 'pristine',
+    dirty: 'dirty'
+  });
+  expect(field.isHeadless).toBe(true);
+  field.classNames = null; // make sure it resets to the default value.
+  field.update({});
+  expect(field.classNames).toEqual({
+    touched: 'touched',
+    untouched: 'untouched',
+    valid: 'valid',
+    invalid: 'invalid',
+    pristine: 'pristine',
+    dirty: 'dirty'
+  });
+});
+
+test('fields can be configured to not add any value listener', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el, { listen: false });
+  expect(field.watchers.length).toBe(2); // just action listeners.
+});
+
+test('caches the field id on the element data attributes', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el);
+
+  // check if id was cached.
+  expect(el.getAttribute('data-vv-id')).toBe(field.id);
+  expect(field.isHeadless).toBe(false);
+});
+
+test('it adds value listeners on the native inputs', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el, { getter: () => el.value });
+
+  // two events [input and blur] along with same events for actions.
+  expect(field.watchers.length).toBe(4);
+  field.addValueListeners(); // Idempotence call.
+  expect(field.watchers.length).toBe(4); // still 4
+
+  field.vm = { $validator: { validate: jest.fn() } };
+
+  // test input event.
+  el.dispatchEvent(new Event('input'));
+  expect(field.validator.validate).toHaveBeenCalledWith(`#${field.id}`, field.value);
+
+  // blur event.
+  el.dispatchEvent(new Event('blur'));
+  expect(field.validator.validate).toHaveBeenCalledTimes(2);
+  expect(field.validator.validate).toHaveBeenLastCalledWith(`#${field.id}`, field.value);
+
+  // test event removal
+  field.unwatch(/input/);
+  // none are left.
+  expect(field.watchers.length).toBe(0);
+  el.dispatchEvent(new Event('blur'));
+
+  // has not changed since it was removed.
+  expect(field.validator.validate).toHaveBeenCalledTimes(2);
+});
+
+test('it adds value listeners on all radio inputs that have the same name', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="1" type="radio">
+    <input name="name" value="2" type="radio">
+    <input name="name" id="last" value="3" type="radio">
+  `;
+  const el = document.querySelector('#name');
+  const el2 = document.querySelector('#last');
+  const field = new Field(el, { getter: () => el.value });
+  field.vm = { $validator: { validate: jest.fn() } };
+  el2.dispatchEvent(new Event('change'));
+
+  expect(field.validator.validate).toHaveBeenCalledTimes(1); // triggered.
+  expect(field.watchers.length).toBe(8); // 4 for each field.
+  field.unwatch();
+  el2.dispatchEvent(new Event('change'));
+  expect(field.watchers.length).toBe(0);
+  expect(field.validator.validate).toHaveBeenCalledTimes(1);
+});
+
+test('it adds value listeners on the components', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el, {
+    component: { $on: jest.fn(), $off: jest.fn() },
+    getters: {
+      context: () => el,
+      value: (ctx) => ctx.value
+    }
+  });
+
+  // two events [input and blur] with the classes.
+  expect(field.watchers.length).toBe(4);
+  expect(field.component.$on).toHaveBeenCalledTimes(2);
+  field.addValueListeners(); // Idempotence call.
+  expect(field.watchers.length).toBe(4); // still 4
+});
+
+
+test('computes the disabled property', () => {
+  document.body.innerHTML = `<input name="name" id="name" value="10">`;
+  const el = document.querySelector('#name');
+  const field = new Field(el);
+
+  expect(field.isDisabled).toBe(false);
+  el.disabled = true;
+  expect(field.isDisabled).toBe(true);
+
+  field.component = { disabled: true };
+  expect(field.isDisabled).toBe(true);
+});
+
+test('computes the display name', () => {
+  const field = new Field(null, { name: 'email' });
+  // no alias was defined.
+  expect(field.displayName).toBe(undefined);
+  field.alias = 'aliased';
+  expect(field.displayName).toBe('aliased');
+});
+
+test('it exposes a boolean to check wether it is a component or not', () => {
+  let field = new Field(null, { component: null });
+  expect(field.isVue).toBe(false);
+  field = new Field(null, { component: {} });
+  expect(field.isVue).toBe(true);
+});
+
+test('it adds class listeners on the input', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10" type="text">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el, { classes: false });
+
+  // input, blur, focus, and another input.
+  expect(field.watchers.length).toBe(4);
+  field.addActionListeners(); // Idempotence call.
+  expect(field.watchers.length).toBe(4); // still 4
+
+  // if field classes are disabled do not add.
+  field.updateClasses();
+  expect(el.classList.contains('untouched')).toBe(false);
+  expect(el.classList.contains('pristine')).toBe(false);
+
+  field.update({ classes: true });
+  expect(el.classList.contains('untouched')).toBe(true);
+  expect(el.classList.contains('pristine')).toBe(true);
+
+
+  // test classes and one shot behavior.
+  el.dispatchEvent(new Event('input'));
+  expect(field.watchers.length).toBe(3); // 1 event down.
+  expect(el.classList.contains('dirty')).toBe(true);
+  expect(field.flags.dirty).toBe(true);
+  expect(el.classList.contains('pristine')).toBe(false);
+  expect(field.flags.pristine).toBe(false);
+
+  el.dispatchEvent(new Event('blur'));
+  expect(field.watchers.length).toBe(2); // 1 more down.
+  expect(el.classList.contains('untouched')).toBe(false);
+  expect(field.flags.untouched).toBe(false);
+  expect(el.classList.contains('touched')).toBe(true);
+  expect(field.flags.touched).toBe(true);
+
+  // disable the classes after being enabled.
+  field.addActionListeners(); // Idempotence call.
+  expect(field.watchers.length).toBe(4); // back to 4.
+  field.update({ classes: false }); // disable classes.
+  expect(field.watchers.length).toBe(4); // they remain because of flags.
+});
+
+test('calls validate if it was already validated and if there is a change in rules', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10" type="text">
+  `;
+  const el = document.querySelector('#name');
+  const vm = { $validator: { validate: jest.fn() } };
+  const field = new Field(el, { rules: 'required', vm });
+
+  // field is updated but hadn't been validated yet.
+  field.update({});
+  expect(vm.$validator.validate).toHaveBeenCalledTimes(0);
+
+  // validated but no change in rules.
+  field.flags.validated = true;
+  field.update({});
+  expect(vm.$validator.validate).toHaveBeenCalledTimes(0);
+
+  // change in rules.
+  field.update({ rules: 'required|alpha' });
+  expect(vm.$validator.validate).toHaveBeenCalledWith(`#${field.id}`);
+});
+
+test('it adds class listeners on components', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10" type="text">
+  `;
+  const component = {
+    evts: [],
+    $once: jest.fn((evt, cb) => {
+      component.evts.push({ evt, cb });
+    }),
+    $off: jest.fn((evt) => {
+      component.evts = component.evts.filter(e => e.evt !== evt);
+    }),
+    $on: jest.fn(),
+    $emit: jest.fn(event => {
+      component.evts.forEach(e => e.cb());
+    })
+  };
+  const el = document.querySelector('#name');
+  const field = new Field(el, {
+    events: '',
+    classes: true,
+    component,
+  });
+
+  expect(field.component.$once).toHaveBeenCalledTimes(2);
+  component.$emit('input');
+  // first call was to off the input listener.
+  expect(field.component.$off.mock.calls[0][0]).toBe('input');
+
+  component.$emit('blur');
+  // second call was to off the blur listener.
+  expect(field.component.$off.mock.calls[1][0]).toBe('blur');
+  expect(field.component.$off).toHaveBeenCalledTimes(2);
+
+  // only once.
+  component.$emit('input');
+  component.$emit('blur');
+  expect(field.component.$off).toHaveBeenCalledTimes(2);
+
+  // no events.
+  expect(field.watchers.length).toBe(0);
+});
+
+test('it checks against a specified matcher', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10" type="text">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el, { name: 'name' });
+  // match by name.
+  expect(field.matches({
+    name: 'name'
+  })).toBe(true);
+
+  // match by scope.
+   expect(field.matches({
+    scope: null
+  })).toBe(true);
+
+  // both
+  expect(field.matches({
+    scope: null,
+    name: 'email'
+  })).toBe(false);
+
+  expect(field.matches({
+    scope: null,
+    name: 'name'
+  })).toBe(true);
+
+  // match by id.
+  expect(field.matches({
+    id: field.id
+  })).toBe(true);
+
+  // no matcher.
+  expect(field.matches({})).toBe(true);
+});
+
+test('uses the watch API instead of adding listeners if the field is bound with a model', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10" type="text">
+  `;
+  const el = document.querySelector('#name');
+  // opt in for only the input event.
+  const field = new Field(el, { model: 'email', events: 'input', vm: { $watch: jest.fn() } });
+
+  // one for model, two for user actions.
+  expect(field.watchers.length).toBe(3);
+  expect(field.watchers[2].tag).toBe('input_model');
+  expect(field.vm.$watch).toHaveBeenCalled();
+});
+
+test('fields can be destroyed and have all their listeners cleaned up along with their dependencies', () => {
+  document.body.innerHTML = `
+    <input name="name" id="name" value="10" type="text">
+  `;
+  const el = document.querySelector('#name');
+  const field = new Field(el, { name: 'name', classes: true });
+  const destroy = jest.fn();
+  field.dependencies.push({ field: { destroy }});
+  expect(field.watchers.length).toBe(4);
+  field.destroy();
+  expect(field.watchers.length).toBe(0);
+  expect(destroy).toHaveBeenCalled();
+})
+
+test('sets aria attributes on elements', async () => {
+  let el = document.createElement('input');
+  const field = new Field(el, { rules: 'required' });
+  expect(el.getAttribute('aria-required')).toBe('true');
+  expect(el.getAttribute('aria-invalid')).toBe('false');
+
+  field.flags.invalid = true;
+  field.update({ rules: 'min:3' });
+  expect(el.getAttribute('aria-required')).toBe('false');
+  expect(el.getAttribute('aria-invalid')).toBe('true');
+  field.el = null; // headless
+  field.updateAriaAttrs();
+
+  // unchanged.
+  expect(el.getAttribute('aria-required')).toBe('false');
+  expect(el.getAttribute('aria-invalid')).toBe('true');
+});
+
+test('computes the rejectsFalse property', () => {
+  let el = document.createElement('input');
+  el.type = 'checkbox';
+  const vm = {
+    $validator: {
+      errors: {
+        update: jest.fn()
+      }
+    }
+  };
+  const component = {
+    value: 10,
+    events: {},
+    $el: document.body.children[0],
+    $watch: () => {},
+    $on: jest.fn()
+  };
+  let field = new Field(null);
+  expect(field.rejectsFalse).toBe(false);
+  field = new Field(null, {
+    component,
+    vm
+  });
+  expect(field.rejectsFalse).toBe(false);
+  field = new Field(el, { rules: 'required', vm });
+  expect(field.rejectsFalse).toBe(true);
+});
+
+test('calls the update method on the validator errors when updating scope', () => {
+  let el = document.createElement('input');
+  const vm = {
+    $validator: {
+      update: jest.fn()
+    }
+  };
+  const field = new Field(el, { rules: 'required', vm });
+  field.updated = true;
+  field.update({ rules: 'min:3', scope: 'scope' });
+  expect(vm.$validator.update).toHaveBeenCalledTimes(1);
+});
+
+test('calls the update method on the validator errors when updating scope to 0', () => {
+  let el = document.createElement('input');
+  const vm = {
+    $validator: {
+      update: jest.fn()
+    }
+  };
+  const field = new Field(el, { rules: 'required', vm });
+  field.updated = true;
+  field.update({ rules: 'min:3', scope: 0 });
+  expect(vm.$validator.update).toHaveBeenCalledTimes(1);
+});
+
+describe('fields can track their dependencies', () => {
+  test('native input depencies', () => {
+    document.body.innerHTML = `
+      <input name="other" id="name" value="10" type="text">
+    `;
+    let field = new Field(null, {
+      rules: 'required|confirmed:other',
+      vm: {
+        $el: document.body,
+        $validator: { validate: jest.fn() }
+      }
+    });
+    expect(field.dependencies.length).toBe(1);
+    expect(field.dependencies[0].name).toBe('confirmed');
+    expect(field.dependencies[0].field.value).toBe('10');
+    const destroy = jest.fn();
+    field.dependencies[0].field.destroy = destroy;
+    field.updateDependencies(); // test destroy.
+
+    expect(destroy).toHaveBeenCalled();
+  });
+
+  test('component dependencies', () => {
+    const component = {
+      value: 10,
+      $el: document.body.children[0],
+      $watch: () => {},
+      $on: () => {}
+    };
+
+    const field = new Field(null, {
+      rules: 'required|confirmed:$other',
+      vm: {
+        $el: document.body,
+        $refs: {
+          other: component
+        }
+      }
+    });
+
+    expect(field.dependencies.length).toBe(1);
+    expect(field.dependencies[0].name).toBe('confirmed');
+    expect(field.dependencies[0].field.value).toBe(10);
+  });
+
+  test('skips if no dependency was resolved', () => {
+    document.body.innerHTML = ``;
+    global.console.warn = jest.fn();
+    let field = new Field(null, {
+      rules: 'required|confirmed:other',
+      vm: {
+        $el: document.body
+      }
+    });
+
+    expect(field.dependencies.length).toBe(0);
+  });
+
+  test('validation triggers on the parent/controller field', () => {
+    document.body.innerHTML = `
+      <input name="other" id="name" value="10" type="text">
+    `;
+    const el = document.querySelector('#name');
+    const field = new Field(null, {
+      rules: 'required|confirmed:other',
+      vm: {
+        $el: document.body,
+        $validator: { validate: jest.fn() }
+      }
+    });
+
+    el.dispatchEvent(new Event('input'));
+    expect(field.dependencies[0].field.validator.validate).toHaveBeenCalledWith(`#${field.id}`);
+  });
+
+  test('fails silently with invalid selectors', () => {
+    document.body.innerHTML = `
+      <input name="other" id="name" value="10" type="text">
+    `;
+    let field = new Field(null, {
+      rules: 'required|confirmed:123-123-123',
+      vm: {
+        $el: document.body,
+        $validator: { validate: jest.fn() }
+      }
+    });
+    expect(field.dependencies.length).toBe(0);
+  });
+});
+
+test('set field custom validity if possible', () => {
+  document.body.innerHTML = `
+      <input name="other" id="name" value="10" type="text">
+    `;
+  const el = document.querySelector('#name');
+  const field = new Field(null, {
+    rules: 'required|confirmed:other',
+    vm: {
+      $el: document.body,
+      $validator: { validate: jest.fn(), errors: { firstById: () => 'Error' } }
+    }
+  });
+
+  el.setCustomValidity = jest.fn();
+  field.flags.valid = false;
+  field.flags.invalid = true;
+
+  field.updateCustomValidity();
+  expect(el.setCustomValidity).toHaveBeenCalledTimes(0);
+
+  field.el = el;
+  field.updateCustomValidity();
+  expect(el.setCustomValidity).toHaveBeenCalledTimes(1);
+
+  field.validator.errors.firstById = () => null;
+  // check unset.
+  field.flags.valid = true;
+  field.updateCustomValidity();
+  expect(el.setCustomValidity).toHaveBeenLastCalledWith('');
+
+  // Check set with no errors in error bag.
+  field.flags.valid = false;
+  field.updateCustomValidity();
+  expect(el.setCustomValidity).toHaveBeenLastCalledWith('');
+});
+
+test('resets field state', () => {
+  const el = document.createElement('input');
+  el.type = 'text';
+  el.name = 'field';
+  const vm = {
+    $validator: {
+      errors: {
+        removeById: jest.fn()
+      }
+    }
+  };
+  const field = new Field(el, { vm });
+  field.flags.valid = true;
+  field.el.setAttribute('aria-invalid', 'true');
+
+  field.reset();
+  expect(field.flags.valid).toBe(null);
+  expect(field.el.getAttribute('aria-invalid')).toBe('false');
+});
+
+test('sets the field flags', () => {
+  const field = new Field(null);
+  field.setFlags({
+    valid: false
+  });
+  expect(field.flags.invalid).toBe(true);
+  expect(field.flags.valid).toBe(false);
+
+  // specifiying the negated flag is respected.
+  field.setFlags({
+    touched: false,
+    untouched: false
+  });
+
+  expect(field.flags.touched).toBe(false);
+  expect(field.flags.untouched).toBe(false);
+});
+
+test('field scope set to 0', () => {
+  const field = new Field(null, {scope:0});
+  expect(field.scope).toBe(0);
+});
diff --git a/tests/generator.js b/tests/generator.js
index d06104c2..d7d85042 100644
--- a/tests/generator.js
+++ b/tests/generator.js
@@ -1,379 +1,379 @@
-import Generator from '../src/core/generator';
-
-test('resolves the bound model', () => {
-  // watchable model.
-  let vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
-  expect(Generator.resolveModel({ arg: null, value: null }, vnode)).toBe('email');
-
-  // watchable directive arg
-  vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
-  expect(Generator.resolveModel({ arg: 'email', value: null }, vnode)).toBe('email');
-
-  // watchable expression arg
-  vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
-  expect(Generator.resolveModel({ arg: null, value: { arg: 'email' } }, vnode)).toBe('email');
-
-  // old model when was included in directives array.
-  vnode = { context: { email: '' }, data: { directives: [{ name: 'model', expression: 'email' }] } };
-  expect(Generator.resolveModel({}, vnode)).toBe('email');
-
-
-  // unresolvable.
-  vnode = { context: { email: '' }, data: { directives: [] } };
-  expect(Generator.resolveModel({}, vnode)).toBe(null);
-
-  // unwatchable model because it does not exist on the vm (context).
-  vnode = { context: {}, data: { model: { expression: 'email' } } };
-  expect(Generator.resolveModel({ arg: null, value: null }, vnode)).toBe(null);
-
-  // Part of a `v-for` cannot be watched by the $watch API.
-  vnode = { context: {}, data: { model: { expression: 'emails[0]' } } };
-  expect(Generator.resolveModel({ arg: null, value: null }, vnode)).toBe(null);
-});
-
-test('resolves the input scope', () => {
-  // defined on element.
-  document.body.innerHTML = `
-    <input type="text" name="field" id="el" v-model="email" data-vv-scope="s1">
-  `;
-  let el = document.querySelector('#el');
-  let vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
-  expect(Generator.resolveScope(el, { arg: null, value: null })).toBe('s1');
-
-  // defined on form.
-  document.body.innerHTML = `
-    <form data-vv-scope="s2">
-      <input type="text" name="field" id="el" v-model="email">
-    </form>
-  `;
-  el = document.querySelector('#el');
-  expect(Generator.resolveScope(el, { arg: null, value: null })).toBe('s2');
-
-  // defined in expression.
-  el = document.querySelector('#el');
-  expect(Generator.resolveScope(el, { arg: null, value: { scope: 's3'} })).toBe('s3');
-
-  el = document.querySelector('#el');
-  expect(Generator.resolveScope(el, { arg: null, value: { scope: 0} })).toBe(0);
-
-  // defined in a components $attrs.
-  el = document.querySelector('#el');
-  vnode = { child: { $attrs: { 'data-vv-scope': 's4' } } };
-  expect(Generator.resolveScope(el, { arg: null, value: null }, vnode)).toBe('s4');
-
-  // define in a component's $el
-  vnode = { child: { $attrs: {} } };
-  expect(Generator.resolveScope(el, { arg: null, value: null }, vnode)).toBe('s2');
-});
-
-test('resolves delay', () => {
-  document.body.innerHTML = `
-    <input type="text" name="field" id="el" v-model="email" data-vv-delay="100">
-  `;
-  const vnode = { child: { $attrs: { 'data-vv-delay': '200' } } };
-  let el = document.querySelector('#el');
-  expect(Generator.resolveDelay(el, {})).toBe('100');
-
-  el = { getAttribute: () => null };
-  expect(Generator.resolveDelay(el, vnode)).toBe('200');
-  expect(Generator.resolveDelay(el, {}, { delay: '300' })).toBe('300');
-})
-
-test('resolves events', () => {
-  document.body.innerHTML = `
-    <input type="text" name="field" id="el" v-model="email" data-vv-validate-on="input">
-  `;
-  let el = document.querySelector('#el');
-  const vnode = { child: { $attrs: { 'data-vv-validate-on': 'focus' } } };
-  expect(Generator.resolveEvents(el, {})).toBe('input');
-  el = { getAttribute: () => null };
-
-  expect(Generator.resolveEvents(el, vnode)).toBe('focus');
-});
-
-test('resolves alias', () => {
-  document.body.innerHTML = `
-    <input type="text" name="field" id="el" v-model="email" data-vv-as="myAlias">
-  `;
-  let el = document.querySelector('#el');
-  const vnode = { child: { $attrs: { 'data-vv-as': 'alias' } } };
-  expect(Generator.resolveAlias(el, {})()).toBe('myAlias');
-  el = { getAttribute: () => null };
-
-  expect(Generator.resolveAlias(el, vnode)()).toBe('alias');
-});
-
-describe('resolves the value getters', () => {
-  test('resolves for text fields', () => {
-    document.body.innerHTML = `
-      <input type="text" name="field" id="el" value="somevalue">
-    `;
-    let el = document.querySelector('#el');
-    let vnode = { context: { email: '' }, data: {} };
-    const getter = Generator.resolveGetter(el, vnode);
-    expect(
-      getter()
-    ).toBe('somevalue');
-  });
-
-  test('resolves for radio inputs', () => {
-    document.body.innerHTML = `
-      <input type="radio" name="field" id="el1" value="some">
-      <input type="radio" name="field" id="el2" value="other">
-      <input type="radio" name="field" id="el3" value="value">
-    `;
-
-    const els = [
-      document.querySelector('#el1'),
-      document.querySelector('#el2'),
-      document.querySelector('#el3')
-    ];
-    const vnode = { context: { email: '' }, data: {} };
-    const getter = Generator.resolveGetter(els[0], vnode);
-
-    // none checked.
-    expect(
-      getter()
-    ).toBe(undefined);
-
-    els[0].checked = true;
-    expect(
-      getter()
-    ).toBe('some');
-
-
-    els[1].checked = true;
-    expect(
-      getter()
-    ).toBe('other');
-
-
-    els[2].checked = true;
-    expect(
-      getter()
-    ).toBe('value');
-  });
-
-  test('resolves for checkboxes', () => {
-    document.body.innerHTML = `
-      <input type="checkbox" name="field[]" id="el1" value="some">
-      <input type="checkbox" name="field[]" id="el2" value="other">
-      <input type="checkbox" name="field[]" id="el3" value="value">
-    `;
-
-    const els = [
-      document.querySelector('#el1'),
-      document.querySelector('#el2'),
-      document.querySelector('#el3')
-    ];
-    const vnode = { context: { email: '' }, data: {} };
-    const getter = Generator.resolveGetter(els[0], vnode);
-    // none checked.
-    expect(
-      getter()
-    ).toBe(undefined);
-
-    els[0].checked = true;
-    expect(
-      getter()
-    ).toEqual(['some']);
-
-    els[1].checked = true;
-    expect(
-      getter()
-    ).toEqual(['some', 'other']);
-
-    els[2].checked = true;
-    expect(
-      getter()
-    ).toEqual(['some', 'other', 'value']);
-  });
-
-  test('resolves for select one field', () => {
-    document.body.innerHTML = `
-      <select type="text" name="field" id="el">
-        <option value="1">1</option>
-        <option selected value="2">2</option>
-        <option value="3">3</option>
-      </select>
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: { email: '' }, data: {} };
-    const getter = Generator.resolveGetter(el, vnode);
-    expect(getter()).toBe('2');
-  });
-
-  test('resolves for select multiple field', () => {
-    document.body.innerHTML = `
-      <select type="text" name="field" id="el" multiple>
-        <option value="1">1</option>
-        <option selected value="2">2</option>
-        <option selected value="3">3</option>
-      </select>
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: { email: '' }, data: {} };
-    const getter = Generator.resolveGetter(el, vnode);
-    expect(getter()).toEqual(['2', '3']);
-  });
-
-  test('resolves for file inputs', () => {
-    document.body.innerHTML = `
-      <input id="el" type="file">
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: { email: '' }, data: {} };
-    const getter = Generator.resolveGetter(el, vnode);
-    expect(getter()).toEqual([]);
-  });
-
-  test('it resolves for a model bound field', () => {
-    document.body.innerHTML = `
-      <input type="text" name="field" id="el" v-model="email">
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: { email: 'example@example.com' }, data: { model: { expression: 'email' } } };
-    const model = Generator.resolveModel({ arg: null, value: null }, vnode); // model must be resolved first.
-    const getter = Generator.resolveGetter(el, vnode, model);
-    expect(getter()).toBe('example@example.com');
-    vnode.context.email = 'other@example.com';
-    expect(getter()).toBe('other@example.com');
-  });
-
-  test('it resolves for a custom component', () => {
-    document.body.innerHTML = `
-      <input type="text" name="field" id="el" data-vv-value-path="someval">
-    `;
-    let el = document.querySelector('#el');
-    const vnode = { context: {}, child: { someval: 'test', value: 'test2', third: 33 }, data: {} };
-    let getter = Generator.resolveGetter(el, vnode);
-    expect(getter()).toBe('test');
-    vnode.child.someval = 'changed';
-    expect(getter()).toBe('changed');
-
-    // test fallback to value property.
-    el.setAttribute('data-vv-value-path', '');
-    getter = Generator.resolveGetter(el, vnode);
-    expect(getter()).toBe('test2');
-    vnode.child.value = 'changed';
-    expect(getter()).toBe('changed');
-    delete vnode.child.value;
-
-    vnode.child.$attrs = { 'data-vv-value-path': 'third' };
-    expect(Generator.resolveGetter(el, vnode)()).toBe(33);
-  });
-});
-
-test('resolves the rules', () => {
-  document.body.innerHTML = `
-    <input type="text" name="el" id="el" data-vv-rules="required|email">
-  `;
-  const el = document.querySelector('#el');
-  expect(Generator.resolveRules(el, {})).toBe('required|email');
-
-  const expression = 'someexpr';
-  let value = 'required|email';
-
-  expect(Generator.resolveRules(el, { expression, value })).toBe('required|email');
-
-  value = {
-    rules: {
-      required: true,
-      email: true
-    }
-  };
-
-  expect(Generator.resolveRules(el, { expression, value })).toEqual({
-    required: true,
-    email: true
-  });
-
-  value = {
-    required: true,
-    email: true
-  };
-
-  expect(Generator.resolveRules(el, { expression, value })).toEqual({
-    required: true,
-    email: true
-  });
-});
-
-describe('resolves the field name', () => {
-  test('using name attribute', () => {
-    document.body.innerHTML = `
-      <input type="text" name="field" id="el">
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: { email: '' }, data: {} };
-    expect(Generator.resolveName(el, vnode)).toBe('field');
-  });
-
-  test('using data-vv-name attribute', () => {
-    document.body.innerHTML = `
-      <input type="text" data-vv-name="field" id="el">
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: {}, data: {} };
-    expect(Generator.resolveName(el, vnode)).toBe('field');
-  });
-
-  test('custom component names', () => {
-    document.body.innerHTML = `
-      <input type="text" id="el">
-    `;
-    const el = document.querySelector('#el');
-    const vnode = { context: {}, child: { name: 'component' }, data: {} };
-    expect(Generator.resolveName(el, vnode)).toBe('component');
-    el.setAttribute('data-vv-name', 'field'); // data-vv-name takes priority.
-    vnode.child.name = null;
-    expect(Generator.resolveName(el, vnode)).toBe('field');
-    el.setAttribute('data-vv-name', ''); // data-vv-name takes priority.
-    vnode.child.$attrs = { 'data-vv-name': 'attrName' }; // simulate Vue's 2.4 inheritAttrs set to false.
-    expect(Generator.resolveName(el, vnode)).toBe('attrName');
-
-    // if it doesn't expose the name as a prop in Vue 2.4+
-    vnode.child.$attrs = { "name": "attrName1" };
-    expect(Generator.resolveName(el, vnode)).toBe("attrName1");
-  });
-});
-
-test('generates field options', () => {
-  document.body.innerHTML = `
-    <input type="text" name="field" id="el" v-model="email" data-vv-scope="s1">
-  `;
-  const el = document.querySelector('#el');
-  const vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
-  expect(Generator.generate(el, { expression: true, value: 'required|max:3', modifiers: {} }, vnode)).toMatchObject({
-    name: 'field',
-    scope: 's1',
-    model: 'email',
-    vm: {},
-    component: undefined,
-    classes: false,
-    classNames: null,
-    expression: 'required|max:3',
-    rules: 'required|max:3',
-    initial: false,
-    el
-  });
-});
-
-test('generates fake vm', () => {
-  const vnode = {
-    context: {
-      $el: 'this is an el',
-      $refs: { myref: 1 },
-      $watch: jest.fn(),
-      $validator: {
-        validate: jest.fn(),
-        errors: {},
-        update: jest.fn()
-      }
-    }
-  };
-
-  const vm = Generator.makeVM(vnode.context);
-  expect(vm.$el).toBe('this is an el');
-  expect(vm.$refs.myref).toBe(1);
-});
+import Generator from '../src/core/generator';
+
+test('resolves the bound model', () => {
+  // watchable model.
+  let vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
+  expect(Generator.resolveModel({ arg: null, value: null }, vnode)).toBe('email');
+
+  // watchable directive arg
+  vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
+  expect(Generator.resolveModel({ arg: 'email', value: null }, vnode)).toBe('email');
+
+  // watchable expression arg
+  vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
+  expect(Generator.resolveModel({ arg: null, value: { arg: 'email' } }, vnode)).toBe('email');
+
+  // old model when was included in directives array.
+  vnode = { context: { email: '' }, data: { directives: [{ name: 'model', expression: 'email' }] } };
+  expect(Generator.resolveModel({}, vnode)).toBe('email');
+
+
+  // unresolvable.
+  vnode = { context: { email: '' }, data: { directives: [] } };
+  expect(Generator.resolveModel({}, vnode)).toBe(null);
+
+  // unwatchable model because it does not exist on the vm (context).
+  vnode = { context: {}, data: { model: { expression: 'email' } } };
+  expect(Generator.resolveModel({ arg: null, value: null }, vnode)).toBe(null);
+
+  // Part of a `v-for` cannot be watched by the $watch API.
+  vnode = { context: {}, data: { model: { expression: 'emails[0]' } } };
+  expect(Generator.resolveModel({ arg: null, value: null }, vnode)).toBe(null);
+});
+
+test('resolves the input scope', () => {
+  // defined on element.
+  document.body.innerHTML = `
+    <input type="text" name="field" id="el" v-model="email" data-vv-scope="s1">
+  `;
+  let el = document.querySelector('#el');
+  let vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
+  expect(Generator.resolveScope(el, { arg: null, value: null })).toBe('s1');
+
+  // defined on form.
+  document.body.innerHTML = `
+    <form data-vv-scope="s2">
+      <input type="text" name="field" id="el" v-model="email">
+    </form>
+  `;
+  el = document.querySelector('#el');
+  expect(Generator.resolveScope(el, { arg: null, value: null })).toBe('s2');
+
+  // defined in expression.
+  el = document.querySelector('#el');
+  expect(Generator.resolveScope(el, { arg: null, value: { scope: 's3'} })).toBe('s3');
+
+  el = document.querySelector('#el');
+  expect(Generator.resolveScope(el, { arg: null, value: { scope: 0} })).toBe(0);
+
+  // defined in a components $attrs.
+  el = document.querySelector('#el');
+  vnode = { child: { $attrs: { 'data-vv-scope': 's4' } } };
+  expect(Generator.resolveScope(el, { arg: null, value: null }, vnode)).toBe('s4');
+
+  // define in a component's $el
+  vnode = { child: { $attrs: {} } };
+  expect(Generator.resolveScope(el, { arg: null, value: null }, vnode)).toBe('s2');
+});
+
+test('resolves delay', () => {
+  document.body.innerHTML = `
+    <input type="text" name="field" id="el" v-model="email" data-vv-delay="100">
+  `;
+  const vnode = { child: { $attrs: { 'data-vv-delay': '200' } } };
+  let el = document.querySelector('#el');
+  expect(Generator.resolveDelay(el, {})).toEqual(expect.objectContaining({ local: { input: 100 } }));
+
+  el = { getAttribute: () => null };
+  expect(Generator.resolveDelay(el, vnode)).toEqual(expect.objectContaining({ local: { input: 200 } }));
+  expect(Generator.resolveDelay(el, {}, { delay: '300' })).toEqual({ global: 300 });
+})
+
+test('resolves events', () => {
+  document.body.innerHTML = `
+    <input type="text" name="field" id="el" v-model="email" data-vv-validate-on="input">
+  `;
+  let el = document.querySelector('#el');
+  const vnode = { child: { $attrs: { 'data-vv-validate-on': 'focus' } } };
+  expect(Generator.resolveEvents(el, {})).toBe("input");
+  el = { getAttribute: () => null };
+
+  expect(Generator.resolveEvents(el, vnode)).toBe("focus");
+});
+
+test('resolves alias', () => {
+  document.body.innerHTML = `
+    <input type="text" name="field" id="el" v-model="email" data-vv-as="myAlias">
+  `;
+  let el = document.querySelector('#el');
+  const vnode = { child: { $attrs: { 'data-vv-as': 'alias' } } };
+  expect(Generator.resolveAlias(el, {})()).toBe('myAlias');
+  el = { getAttribute: () => null };
+
+  expect(Generator.resolveAlias(el, vnode)()).toBe('alias');
+});
+
+describe('resolves the value getters', () => {
+  test('resolves for text fields', () => {
+    document.body.innerHTML = `
+      <input type="text" name="field" id="el" value="somevalue">
+    `;
+    let el = document.querySelector('#el');
+    let vnode = { context: { email: '' }, data: {} };
+    const getter = Generator.resolveGetter(el, vnode);
+    expect(
+      getter()
+    ).toBe('somevalue');
+  });
+
+  test('resolves for radio inputs', () => {
+    document.body.innerHTML = `
+      <input type="radio" name="field" id="el1" value="some">
+      <input type="radio" name="field" id="el2" value="other">
+      <input type="radio" name="field" id="el3" value="value">
+    `;
+
+    const els = [
+      document.querySelector('#el1'),
+      document.querySelector('#el2'),
+      document.querySelector('#el3')
+    ];
+    const vnode = { context: { email: '' }, data: {} };
+    const getter = Generator.resolveGetter(els[0], vnode);
+
+    // none checked.
+    expect(
+      getter()
+    ).toBe(undefined);
+
+    els[0].checked = true;
+    expect(
+      getter()
+    ).toBe('some');
+
+
+    els[1].checked = true;
+    expect(
+      getter()
+    ).toBe('other');
+
+
+    els[2].checked = true;
+    expect(
+      getter()
+    ).toBe('value');
+  });
+
+  test('resolves for checkboxes', () => {
+    document.body.innerHTML = `
+      <input type="checkbox" name="field[]" id="el1" value="some">
+      <input type="checkbox" name="field[]" id="el2" value="other">
+      <input type="checkbox" name="field[]" id="el3" value="value">
+    `;
+
+    const els = [
+      document.querySelector('#el1'),
+      document.querySelector('#el2'),
+      document.querySelector('#el3')
+    ];
+    const vnode = { context: { email: '' }, data: {} };
+    const getter = Generator.resolveGetter(els[0], vnode);
+    // none checked.
+    expect(
+      getter()
+    ).toBe(undefined);
+
+    els[0].checked = true;
+    expect(
+      getter()
+    ).toEqual(['some']);
+
+    els[1].checked = true;
+    expect(
+      getter()
+    ).toEqual(['some', 'other']);
+
+    els[2].checked = true;
+    expect(
+      getter()
+    ).toEqual(['some', 'other', 'value']);
+  });
+
+  test('resolves for select one field', () => {
+    document.body.innerHTML = `
+      <select type="text" name="field" id="el">
+        <option value="1">1</option>
+        <option selected value="2">2</option>
+        <option value="3">3</option>
+      </select>
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: { email: '' }, data: {} };
+    const getter = Generator.resolveGetter(el, vnode);
+    expect(getter()).toBe('2');
+  });
+
+  test('resolves for select multiple field', () => {
+    document.body.innerHTML = `
+      <select type="text" name="field" id="el" multiple>
+        <option value="1">1</option>
+        <option selected value="2">2</option>
+        <option selected value="3">3</option>
+      </select>
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: { email: '' }, data: {} };
+    const getter = Generator.resolveGetter(el, vnode);
+    expect(getter()).toEqual(['2', '3']);
+  });
+
+  test('resolves for file inputs', () => {
+    document.body.innerHTML = `
+      <input id="el" type="file">
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: { email: '' }, data: {} };
+    const getter = Generator.resolveGetter(el, vnode);
+    expect(getter()).toEqual([]);
+  });
+
+  test('it resolves for a model bound field', () => {
+    document.body.innerHTML = `
+      <input type="text" name="field" id="el" v-model="email">
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: { email: 'example@example.com' }, data: { model: { expression: 'email' } } };
+    const model = Generator.resolveModel({ arg: null, value: null }, vnode); // model must be resolved first.
+    const getter = Generator.resolveGetter(el, vnode, model);
+    expect(getter()).toBe('example@example.com');
+    vnode.context.email = 'other@example.com';
+    expect(getter()).toBe('other@example.com');
+  });
+
+  test('it resolves for a custom component', () => {
+    document.body.innerHTML = `
+      <input type="text" name="field" id="el" data-vv-value-path="someval">
+    `;
+    let el = document.querySelector('#el');
+    const vnode = { context: {}, child: { someval: 'test', value: 'test2', third: 33 }, data: {} };
+    let getter = Generator.resolveGetter(el, vnode);
+    expect(getter()).toBe('test');
+    vnode.child.someval = 'changed';
+    expect(getter()).toBe('changed');
+
+    // test fallback to value property.
+    el.setAttribute('data-vv-value-path', '');
+    getter = Generator.resolveGetter(el, vnode);
+    expect(getter()).toBe('test2');
+    vnode.child.value = 'changed';
+    expect(getter()).toBe('changed');
+    delete vnode.child.value;
+
+    vnode.child.$attrs = { 'data-vv-value-path': 'third' };
+    expect(Generator.resolveGetter(el, vnode)()).toBe(33);
+  });
+});
+
+test('resolves the rules', () => {
+  document.body.innerHTML = `
+    <input type="text" name="el" id="el" data-vv-rules="required|email">
+  `;
+  const el = document.querySelector('#el');
+  expect(Generator.resolveRules(el, {})).toBe('required|email');
+
+  const expression = 'someexpr';
+  let value = 'required|email';
+
+  expect(Generator.resolveRules(el, { expression, value })).toBe('required|email');
+
+  value = {
+    rules: {
+      required: true,
+      email: true
+    }
+  };
+
+  expect(Generator.resolveRules(el, { expression, value })).toEqual({
+    required: true,
+    email: true
+  });
+
+  value = {
+    required: true,
+    email: true
+  };
+
+  expect(Generator.resolveRules(el, { expression, value })).toEqual({
+    required: true,
+    email: true
+  });
+});
+
+describe('resolves the field name', () => {
+  test('using name attribute', () => {
+    document.body.innerHTML = `
+      <input type="text" name="field" id="el">
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: { email: '' }, data: {} };
+    expect(Generator.resolveName(el, vnode)).toBe('field');
+  });
+
+  test('using data-vv-name attribute', () => {
+    document.body.innerHTML = `
+      <input type="text" data-vv-name="field" id="el">
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: {}, data: {} };
+    expect(Generator.resolveName(el, vnode)).toBe('field');
+  });
+
+  test('custom component names', () => {
+    document.body.innerHTML = `
+      <input type="text" id="el">
+    `;
+    const el = document.querySelector('#el');
+    const vnode = { context: {}, child: { name: 'component' }, data: {} };
+    expect(Generator.resolveName(el, vnode)).toBe('component');
+    el.setAttribute('data-vv-name', 'field'); // data-vv-name takes priority.
+    vnode.child.name = null;
+    expect(Generator.resolveName(el, vnode)).toBe('field');
+    el.setAttribute('data-vv-name', ''); // data-vv-name takes priority.
+    vnode.child.$attrs = { 'data-vv-name': 'attrName' }; // simulate Vue's 2.4 inheritAttrs set to false.
+    expect(Generator.resolveName(el, vnode)).toBe('attrName');
+
+    // if it doesn't expose the name as a prop in Vue 2.4+
+    vnode.child.$attrs = { "name": "attrName1" };
+    expect(Generator.resolveName(el, vnode)).toBe("attrName1");
+  });
+});
+
+test('generates field options', () => {
+  document.body.innerHTML = `
+    <input type="text" name="field" id="el" v-model="email" data-vv-scope="s1">
+  `;
+  const el = document.querySelector('#el');
+  const vnode = { context: { email: '' }, data: { model: { expression: 'email' } } };
+  expect(Generator.generate(el, { expression: true, value: 'required|max:3', modifiers: {} }, vnode)).toMatchObject({
+    name: 'field',
+    scope: 's1',
+    model: 'email',
+    vm: {},
+    component: undefined,
+    classes: false,
+    classNames: null,
+    expression: 'required|max:3',
+    rules: 'required|max:3',
+    initial: false,
+    el
+  });
+});
+
+test('generates fake vm', () => {
+  const vnode = {
+    context: {
+      $el: 'this is an el',
+      $refs: { myref: 1 },
+      $watch: jest.fn(),
+      $validator: {
+        validate: jest.fn(),
+        errors: {},
+        update: jest.fn()
+      }
+    }
+  };
+
+  const vm = Generator.makeVM(vnode.context);
+  expect(vm.$el).toBe('this is an el');
+  expect(vm.$refs.myref).toBe(1);
+});
diff --git a/tests/utils.js b/tests/utils.js
index 8d037ff1..7ee21054 100644
--- a/tests/utils.js
+++ b/tests/utils.js
@@ -135,7 +135,7 @@ test('toggles classes', () => {
 
   utils.toggleClass(el, 'some', false);
   expect(utils.hasClass(el, 'some')).toBe(false);
-  
+
   expect(() => {
     utils.toggleClass(el, null, true);
   }).not.toThrow();
@@ -185,7 +185,7 @@ test('gets the value path with a fallback value', () => {
 
   expect(utils.getPath(null, some)).toBe(undefined); // no path.
   expect(utils.getPath('value.val', null)).toBe(undefined); // no object.
-  
+
   expect(utils.getPath('value.val', some)).toBe(1); // exists.
   expect(utils.getPath('value.path', some)).toBe(undefined); // undefined but exists.
   expect(utils.getPath('value.not', some, false)).toBe(false); // does not.
@@ -391,3 +391,65 @@ describe('pareses date values', () => {
   });
 
 });
+
+describe('makeEventsArray', () => {
+  test('it creates valid event arrays', () => {
+    expect(utils.makeEventsArray('input|blur|change')).toEqual(['input', 'blur', 'change']);
+    expect(utils.makeEventsArray('focus')).toEqual(['focus'])
+  });
+
+  test('it handles empty event strings', () => {
+    expect(utils.makeEventsArray('')).toEqual([]);
+  });
+
+  test('it handles invalid event strings', () => {
+    expect(utils.makeEventsArray('input, focus')).not.toEqual(['input', 'focus']);
+    expect(utils.makeEventsArray('blur/change')).toEqual(['blur/change']);
+  });
+});
+
+describe('makeDelayObject', () => {
+  test('it handles delays from an object under the global key', () => {
+    expect(utils.makeDelayObject(['input', 'blur'], {global: {input: 100, blur: 200}})).toEqual({input: 100, blur: 200});
+    expect(utils.makeDelayObject(['change'], {global: {change: 100}})).toEqual({change: 100});
+  });
+
+  test('it handles delays from an object under the local key', () => {
+    expect(utils.makeDelayObject(['input'], {local: {input: 100}})).toEqual({input: 100});
+  });
+
+  test('it handles overwrites values from the global key with those from local key', () => {
+    expect(utils.makeDelayObject(['input'], {local: {input: 100}, global: {input: 500}})).toEqual({input: 100});
+    expect(utils.makeDelayObject(['input', 'blur'], {local: {input: 600}, global: {input: 200, blur: 400}})).toEqual({input: 600, blur: 400});
+  });
+
+  test('it handles all events and sets them to 0', () => {
+    expect(utils.makeDelayObject(['change', 'blur', 'focus'], {})).toEqual({change: 0, blur: 0, focus: 0});
+  });
+
+  test('it handles empty events', () => {
+    expect(utils.makeDelayObject([], {global: {input: 100}})).toEqual({});
+  });
+
+  test('it handles empty given objects', () => {
+    expect(utils.makeDelayObject(['input'], {})).toEqual({input: 0});
+  });
+
+  test('it handles empty events and empty given objects together', () => {
+    expect(utils.makeDelayObject([], {})).toEqual({});
+  });
+
+  test('it handles already valid delay objects', () => {
+    expect(utils.makeDelayObject(['input', 'blur'], {change: 200, focus: 800})).toEqual({change: 200, focus: 800});
+  });
+
+  test('it handles delay integers', () => {
+    expect(utils.makeDelayObject(['focus', 'input'], 600)).toEqual({focus: 600, input: 600});
+  });
+});
+
+describe('deepParseInt', () => {
+  test('it parses all values on the first level of an object to int', () => {
+    expect(utils.deepParseInt({ blur: "10", input: "400", focus: 300, change: "hello" })).toEqual({ blur: 10, input: 400, focus: 300, change: NaN });
+  });
+});