Repository: baianat/vee-validate
Pull request #1238 (2018-03-28 23:58:54)

Use Vue instance
This PR applies the usage of internal Vue instance to manage events and nextTick callbacks.

codecov[bot]: # [Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=h1) Report
> Merging [#1238](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=desc) into [master](https://codecov.io/gh/baianat/vee-validate/commit/14837e33699b2d775cbe8c06c6a3fb504b9bbda4?src=pr&el=desc) will **decrease** coverage by `0.76%`.
> The diff coverage is `84.21%`.

[![Impacted file tree graph](https://codecov.io/gh/baianat/vee-validate/pull/1238/graphs/tree.svg?src=pr&token=XFzlvHKFP5&width=650&height=150)](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master   #1238      +/-   ##
=========================================
- Coverage   99.17%   98.4%   -0.77%     
=========================================
  Files          50      50              
  Lines        1446    1446              
  Branches      464     460       -4     
=========================================
- Hits         1434    1423      -11     
- Misses         12      23      +11
```


| [Impacted Files](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/core/field.js](https://codecov.io/gh/baianat/vee-validate/pull/1238/diff?src=pr&el=tree#diff-c3JjL2NvcmUvZmllbGQuanM=) | `100% <100%> (ø)` | :arrow_up: |
| [src/core/validator.js](https://codecov.io/gh/baianat/vee-validate/pull/1238/diff?src=pr&el=tree#diff-c3JjL2NvcmUvdmFsaWRhdG9yLmpz) | `96.34% <81.25%> (-3.66%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=footer). Last update [14837e3...4d44c85](https://codecov.io/gh/baianat/vee-validate/pull/1238?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/core/field.js b/src/core/field.js
index 91975d92..700b62ea 100644
--- a/src/core/field.js
+++ b/src/core/field.js
@@ -75,9 +75,9 @@ export default class Field {
   _alias: ?string;
   _delay: number | Object;
 
-  constructor (el: HTMLInputElement | null, options: FieldOptions | MapObject = {}) {
+  constructor (options: FieldOptions | MapObject = {}) {
     this.id = uniqId();
-    this.el = el;
+    this.el = options.el;
     this.updated = false;
     this.dependencies = [];
     this.watchers = [];
@@ -166,7 +166,11 @@ export default class Field {
   /**
    * Determines if the instance matches the options provided.
    */
-  matches (options: FieldMatchOptions): boolean {
+  matches (options: FieldMatchOptions | null): boolean {
+    if (!options) {
+      return true;
+    }
+
     if (options.id) {
       return this.id === options.id;
     }
@@ -365,7 +369,7 @@ export default class Field {
         options.getter = Generator.resolveGetter(el, {});
       }
 
-      this.dependencies.push({ name, field: new Field(options.el, options) });
+      this.dependencies.push({ name, field: new Field(options) });
     });
   }
 
diff --git a/src/core/validator.js b/src/core/validator.js
index fc30c73f..d0eac81d 100644
--- a/src/core/validator.js
+++ b/src/core/validator.js
@@ -9,7 +9,6 @@ import Config from '../config';
 const RULES: { [string]: Rule } = {};
 let STRICT_MODE: boolean = true;
 const TARGET_RULES = ['confirmed', 'after', 'before'];
-const ERRORS = []; // HOLD errors references to trigger regeneration.
 
 export default class Validator {
   strict: boolean;
@@ -22,30 +21,22 @@ export default class Validator {
   clean: () => void;
   reset: (matcher) => Promise<void>;
 
-  constructor (validations?: MapObject, options?: MapObject = { vm: null, fastExit: true }) {
+  constructor (validations?: MapObject, options?: MapObject = { fastExit: true }) {
     this.strict = STRICT_MODE;
     this.errors = new ErrorBag();
-
-    // We are running in SSR Mode. Do not keep a reference. It prevent garbage collection.
-    if (typeof window !== 'undefined') {
-      ERRORS.push(this.errors);
-    }
     this.fields = new FieldBag();
     this.flags = {};
     this._createFields(validations);
     this.paused = false;
     this.fastExit = options.fastExit || false;
     this.ownerId = options.vm && options.vm._uid;
-    // create it statically since we don't need constant access to the vm.
-    this.reset = options.vm && isCallable(options.vm.$nextTick) ? (matcher) => {
-      return new Promise(resolve => {
-        options.vm.$nextTick(() => {
-          options.vm.$nextTick(() => {
-            resolve(this._reset(matcher));
-          });
-        });
-      });
-    } : this._reset;
+    this._localeListener = () => {
+      this.errors.regenerate();
+    };
+
+    if (this._vm) {
+      this._vm.$on('localeChanged', this._localeListener);
+    }
   }
 
   /**
@@ -55,6 +46,10 @@ export default class Validator {
     return Config.dependency('dictionary');
   }
 
+  get _vm () {
+    return Config.dependency('vm');
+  }
+
   /**
    * Static Getter for the dictionary.
    */
@@ -89,8 +84,8 @@ export default class Validator {
   static set locale (value: string): void {
     const hasChanged = value !== Validator.dictionary.locale;
     Validator.dictionary.locale = value;
-    if (hasChanged) {
-      Validator.regenerate();
+    if (hasChanged && Config.dependency('vm')) {
+      Config.dependency('vm').$emit('localeChanged');
     }
   }
 
@@ -126,13 +121,6 @@ export default class Validator {
     }
   }
 
-  /**
-   * Regenerates error messages across all validators.
-   */
-  static regenerate () {
-    ERRORS.forEach(errorBag => errorBag.regenerate());
-  }
-
   /**
    * Removes a rule from the list of validators.
    */
@@ -196,6 +184,7 @@ export default class Validator {
    */
   attach (field: FieldOptions | Field): Field {
     // deprecate: handle old signature.
+    /* istanbul ignore next */
     if (arguments.length > 1) {
       warn('This signature of the attach method has been deprecated, please consult the docs.');
       field = assign({}, {
@@ -207,7 +196,7 @@ export default class Validator {
     // fixes initial value detection with v-model and select elements.
     const value = field.initialValue;
     if (!(field instanceof Field)) {
-      field = new Field(field.el || null, field);
+      field = new Field(field);
     }
 
     this.fields.push(field);
@@ -265,6 +254,21 @@ export default class Validator {
     Validator.extend(name, validator, options);
   }
 
+  reset (matcher) {
+    return new Promise(resolve => {
+      this._vm.$nextTick(() => {
+        this._vm.$nextTick(() => {
+          this.fields.filter(matcher).forEach(field => {
+            field.reset(); // reset field flags.
+            this.errors.remove(field.name, field.scope, field.id);
+          });
+
+          resolve();
+        });
+      });
+    });
+  }
+
   /**
    * Updates a field, updating both errors and flags.
    */
@@ -410,11 +414,7 @@ export default class Validator {
    * Perform cleanup.
    */
   destroy () {
-    // Remove ErrorBag instance.
-    const idx = ERRORS.indexOf(this.errors);
-    if (idx === -1) return;
-
-    ERRORS.splice(idx, 1);
+    this._vm.$off('localeChanged', this._localeListener);
   }
 
   /**
@@ -490,26 +490,6 @@ export default class Validator {
     this.flags = assign({}, this.flags, { [`$${scope}`]: scopeObj });
   }
 
-  /**
-   * Resets fields that matches the matcher options or all fields if not specified.
-   */
-  _reset (matcher?: FieldMatchOptions): Promise<void> {
-    return new Promise(resolve => {
-      if (matcher) {
-        this.fields.filter(matcher).forEach(field => {
-          field.reset(); // reset field flags.
-          this.errors.remove(field.name, field.scope, field.id);
-        });
-
-        return resolve();
-      }
-
-      this.fields.items.forEach(i => i.reset());
-      this.errors.clear();
-      resolve();
-    });
-  }
-
   /**
    * Tests a single input value against a rule.
    */
diff --git a/src/install.js b/src/install.js
index 1a333b3b..a204b1c5 100644
--- a/src/install.js
+++ b/src/install.js
@@ -15,7 +15,9 @@ function install (_Vue, options = {}) {
   }
 
   Vue = _Vue;
+  Config.register('vm', new Vue());
   Config.merge(options);
+
   const { dictionary, i18n } = Config.current;
 
   if (dictionary) {


Test patch:
diff --git a/tests/unit/errorBag.js b/tests/unit/errorBag.js
index d4c42658..58dca9ba 100644
--- a/tests/unit/errorBag.js
+++ b/tests/unit/errorBag.js
@@ -17,6 +17,9 @@ test('adds errors to the collection', () => {
     scope: 's1'
   })
   expect(errors.first('name', 's1')).toBe('Hey');
+
+  // handles undefined params
+  expect(errors.first()).toBe(null);
 });
 
 test('updates error objects by matching against field id', () => {
@@ -373,4 +376,18 @@ test('can regenerate error messages', () => {
   expect(errors.first('field')).toBe('Product is Alpha');
   errors.regenerate();
   expect(errors.first('field')).toBe('Product is Beta');
+});
+
+test('error bag instance is iterable', () => {
+  const errors = new ErrorBag();
+  errors.add('name.example', 'The name is invalid', 'rule1');
+  errors.add('name', 'The name is really invalid', 'rule2', 'example');
+
+  let idx = 0;
+  for (const error of errors) {
+    expect(error.msg).toBe(errors.items[idx].msg);
+    idx++;
+  }
+
+  expect(idx).toBe(2);
 });
\ No newline at end of file
diff --git a/tests/unit/fieldBag.js b/tests/unit/fieldBag.js
index 65fe4d63..c8791397 100644
--- a/tests/unit/fieldBag.js
+++ b/tests/unit/fieldBag.js
@@ -3,7 +3,7 @@ import Field from '../../src/core/field';
 
 test('adds field items to the collection', () => {
   const bag = new FieldBag();
-  const field = new Field(null);
+  const field = new Field({ el: null });
 
   bag.push(field);
   expect(bag.length).toBe(1);
@@ -26,9 +26,9 @@ test('adds field items to the collection', () => {
 
 test('finds the first field that matches a matcher object', () => {
   const bag = new FieldBag();
-  const field1 = new Field(null, { name: 'email', scope: 's1' });
-  const field2 = new Field(null, { name: 'email', scope: 's2' });
-  const field3 = new Field(null, { name: 'email' });
+  const field1 = new Field({ name: 'email', scope: 's1' });
+  const field2 = new Field({ name: 'email', scope: 's2' });
+  const field3 = new Field({ name: 'email' });
   bag.push(field1);
   bag.push(field2);
   bag.push(field3);
@@ -44,9 +44,9 @@ test('finds the first field that matches a matcher object', () => {
 
 test('filters the fields based on a matcher object', () => {
   const bag = new FieldBag();
-  const field1 = new Field(null, { name: 'email', scope: 's1' });
-  const field2 = new Field(null, { name: 'email', scope: 's2' });
-  const field3 = new Field(null, { name: 'email' });
+  const field1 = new Field({ name: 'email', scope: 's1' });
+  const field2 = new Field({ name: 'email', scope: 's2' });
+  const field3 = new Field({ name: 'email' });
   bag.push(field1);
   bag.push(field2);
   bag.push(field3);
@@ -65,9 +65,9 @@ test('filters the fields based on a matcher object', () => {
 
 test('proxies the map method to the underlying array', () => {
   const bag = new FieldBag();
-  const field1 = new Field(null, { name: 'email', scope: 's1' });
-  const field2 = new Field(null, { name: 'email', scope: 's2' });
-  const field3 = new Field(null, { name: 'email' });
+  const field1 = new Field({ name: 'email', scope: 's1' });
+  const field2 = new Field({ name: 'email', scope: 's2' });
+  const field3 = new Field({ name: 'email' });
   bag.push(field1);
   bag.push(field2);
   bag.push(field3);
@@ -76,9 +76,9 @@ test('proxies the map method to the underlying array', () => {
 
 test('removes the first field that matches a matcher object', () => {
   const bag = new FieldBag();
-  const field1 = new Field(null, { name: 'email', scope: 's1' });
-  const field2 = new Field(null, { name: 'email', scope: 's2' });
-  const field3 = new Field(null, { name: 'email' });
+  const field1 = new Field({ name: 'email', scope: 's1' });
+  const field2 = new Field({ name: 'email', scope: 's2' });
+  const field3 = new Field({ name: 'email' });
   bag.push(field1);
   bag.push(field2);
   bag.push(field3);
@@ -98,3 +98,21 @@ test('removes the first field that matches a matcher object', () => {
   bag.remove(field3);
   expect(bag.length).toBe(0);
 });
+
+test('field bag instance is iterable', () => {
+  const bag = new FieldBag();
+  const field1 = new Field({ name: 'email', scope: 's1' });
+  const field2 = new Field({ name: 'email', scope: 's2' });
+  const field3 = new Field({ name: 'email' });
+  bag.push(field1);
+  bag.push(field2);
+  bag.push(field3);
+
+  let idx = 0;
+  for (const field of bag) {
+    expect(field.name).toBe(bag.items[idx].name);
+    idx++;
+  }
+
+  expect(idx).toBe(3);
+});
\ No newline at end of file
diff --git a/tests/unit/validator.js b/tests/unit/validator.js
index 2891073f..10972280 100644
--- a/tests/unit/validator.js
+++ b/tests/unit/validator.js
@@ -1,6 +1,12 @@
+import { createLocalVue } from '@vue/test-utils';
 import Field from '../../src/core/field'
 import helpers from '../helpers';
 import VeeValidate from '../../src/index';
+import Config from '../../src/config';
+
+const Vue = createLocalVue();
+
+Config.register('vm', new Vue());
 
 const Validator = VeeValidate.Validator;
 
@@ -49,7 +55,7 @@ test('can validate single values', async () => {
   expect(await v.validate('title', 'a'.repeat(256))).toBe(false);
 
   v = new Validator();
-  v.attach('el', 'required|min:3', { scope: 'scope' });
+  v.attach({ name: 'el', rules: 'required|min:3', scope: 'scope' });
   expect(await v.validate('scope.el', '12')).toBe(false);
   expect(await v.validate('scope.el', '123')).toBe(true);
 });
@@ -77,11 +83,14 @@ test('can be initialized without validations', () => {
 
 test('accepts rules as an object', async () => {
   const v = new Validator();
-  v.attach('field', {
-    required: true, // test boolean.
-    regex: /.(js|ts)$/, // test single value.
-    min: 5, // test single value.
-    in: ['blabla.js', 'blabla.ts'] // test params
+  v.attach({
+    name: 'field',
+    rules: {
+      required: true, // test boolean.
+      regex: /.(js|ts)$/, // test single value.
+      min: 5, // test single value.
+      in: ['blabla.js', 'blabla.ts'] // test params
+    }
   });
 
   expect(await v.validate('field', '')).toBe(false);
@@ -214,7 +223,7 @@ test('can manually attach new fields', async () => {
   const v = new Validator();
 
   expect(v.fields.find({ name: 'field' })).toBeFalsy();
-  v.attach('field', 'required|min:5');
+  v.attach({ name: 'field', rules: 'required|min:5' });
   expect(v.fields.find({ name: 'field' })).toBeTruthy();
   expect(await v.validate('field', 'less')).toBe(false);
   expect(await v.validate('field', 'not less')).toBe(true);
@@ -222,7 +231,7 @@ test('can manually attach new fields', async () => {
 
 test('can display errors with custom field names', async () => {
   const v = new Validator();
-  v.attach('field', 'min:5', { alias: 'pretty' });
+  v.attach({ name: 'field', rules: 'min:5', alias: 'pretty' });
   await v.validate('field', 'wo');
   expect(v.errors.first('field')).toBe('The pretty field must be at least 5 characters.');
 });
@@ -242,7 +251,7 @@ test('fails when trying to validate a non-existant field when strict mode is tru
 test('can detach fields', () => {
   const v = new Validator();
 
-  v.attach('field', 'required');
+  v.attach({ name: 'field', rules: 'required' });
   expect(v.fields.find({ name: 'field' })).toBeTruthy();
   v.detach('field');
   expect(v.fields.find({ name: 'field' })).toBeFalsy();
@@ -251,7 +260,9 @@ test('can detach fields', () => {
     v.detach('someOtherField');
   }).not.toThrow();
 
-  v.attach('field', 'required', {
+  v.attach({
+    name: 'field',
+    rules: 'required',
     scope: 'myscope'
   });
   v.detach('myscope.field');
@@ -261,9 +272,9 @@ test('can detach fields', () => {
 test('can validate specific scopes', async () => {
   const v = new Validator();
 
-  v.attach('field', 'alpha', { getter: () => '123' });
-  v.attach('field', 'alpha', { scope: 'myscope', getter: () => '123' });
-  v.attach('field', 'alpha', { scope: 'otherscope', getter: () => '123' });
+  v.attach({ name: 'field', rules: 'alpha', getter: () => '123' });
+  v.attach({ name: 'field', rules: 'alpha', scope: 'myscope', getter: () => '123' });
+  v.attach({ name: 'field', rules: 'alpha', scope: 'otherscope', getter: () => '123' });
 
   // only '__global__' scope got validated.
   expect(await v.validateAll()).toBe(false);
@@ -281,8 +292,8 @@ test('can validate specific scopes on an object', async () => {
     'field': 'required'
   });
 
-  v.attach('field', 'required', { scope: 'myscope' })
-  v.attach('anotherfield', 'required', { scope: 'myscope' })
+  v.attach({ name: 'field', rules: 'required', scope: 'myscope' })
+  v.attach({ name: 'anotherfield', rules: 'required', scope: 'myscope' })
 
   // only global scope got validated.
   expect(await v.validateAll({ field: null })).toBe(false);
@@ -304,7 +315,7 @@ test('can find errors by field and rule', async () => {
 test('can extend the validator with a validator function', async () => {
   const v = new Validator();
   v.extend('neg', (value) => Number(value) < 0);
-  v.attach('anotherField', 'neg');
+  v.attach({ name: 'anotherField', rules: 'neg' });
   expect(await v.validate('anotherField', -1)).toBe(true);
   expect(await v.validate('anotherField', 1)).toBe(false);
   expect(v.errors.first('anotherField')).toBe('The anotherField value is not valid.');
@@ -318,7 +329,7 @@ test('can extend the validators for a validator instance', async () => {
 
   Validator.extend('truthy', truthy); // static extend.
   const v = new Validator();
-  v.attach('anotherField', 'truthy');
+  v.attach({ name: 'anotherField', rules: 'truthy' });
   expect(await v.validate('anotherField', 1)).toBe(true);
   expect(await v.validate('anotherField', 0)).toBe(false);
   expect(v.errors.first('anotherField')).toBe('The anotherField field value is not truthy.');
@@ -352,7 +363,7 @@ test('can set the locale statically', async () => {
     }
   });
   const loc = new Validator();
-  loc.attach('name', 'alpha');
+  loc.attach({ name: 'name', rules: 'alpha' });
   await loc.validate('name', '1234');
   expect(loc.locale).toBe('ar');
   expect(loc.errors.first('name')).toBe('البتاعة لازم يكون حروف بس');
@@ -383,7 +394,7 @@ test('can overwrite messages and add translated messages', async () => {
   Validator.localize('ar', { messages: { alpha: (field) => `${field} يجب ان يحتوي على حروف فقط.`} });
   Validator.localize('en', { messages: { alpha: (field) => `${field} is alphabetic.` } });
 
-  loc.attach('first_name', 'alpha');
+  loc.attach({ name: 'first_name', rules: 'alpha' });
   await loc.validate('first_name', '0123');
   expect(loc.errors.first('first_name')).toBe('first_name is alphabetic.');
 
@@ -518,7 +529,7 @@ test('rules can return objects to provide context to the error message', async (
     }
   });
 
-  v.attach('field', 'reason');
+  v.attach({ name: 'field', rules: 'reason' });
   expect(await v.validate('field', 'wow')).toBe(false);
   expect(v.errors.first('field')).toBe('Not correct');
 });
@@ -542,7 +553,7 @@ test('rules can return a promise that resolves to an object to provide context t
     }
   });
 
-  v.attach('reason_field', 'reason_test_promise');
+  v.attach({ name: 'reason_field', rules: 'reason_test_promise' });
   expect(await v.validate('reason_field', 'trigger')).toBe(false);
   expect(v.errors.first('reason_field')).toBe('Not this value');
   expect(await v.validate('reason_field', false)).toBe(false);
@@ -586,9 +597,9 @@ test('calling validate with a string ending with .* will validate the matched sc
 
 test('caliing validateAll with array should validate only matching name', async () => {
   const v = new Validator();
-  v.attach('name', 'required', { getter: () => '123' })
-  v.attach('lname', 'required|alpha', { getter: () => 'AAA' })
-  v.attach('age', 'required', { getter: () => '' })
+  v.attach({ name: 'name', rules: 'required', getter: () => '123' })
+  v.attach({ name: 'lname', rules: 'required|alpha', getter: () => 'AAA' })
+  v.attach({ name: 'age', rules: 'required', getter: () => '' })
   expect(await v.validateAll(['name', 'lname'])).toBe(true);
   expect(await v.validateAll(['name', 'age'])).toBe(false);
 });
@@ -600,7 +611,7 @@ test('can fetch the values using getters when not specifying values in validateA
     return toggle ? 'valid' : '123';
   };
 
-  v.attach('name', 'required|alpha', { getter });
+  v.attach({ name: 'name', rules: 'required|alpha',  getter });
 
   expect(await v.validateAll()).toBe(true);
   toggle = !toggle;
@@ -612,8 +623,8 @@ test('can fetch the values using getters for a specific scope when not specifyin
   const getter1 = () => 'martin';
   const getter2 = () => 'invalid value';
 
-  v1.attach('name', 'required|alpha', { scope: 'scope1', getter: getter1 });
-  v1.attach('name', 'required|alpha', { scope: 'scope2', getter:getter2 });
+  v1.attach({ name: 'name', rules: 'required|alpha', scope: 'scope1', getter: getter1 });
+  v1.attach({ name: 'name', rules: 'required|alpha', scope: 'scope2', getter: getter2 });
 
   expect(await v1.validateAll('scope1')).toBe(true);
   expect(await v1.validateAll('scope2')).toBe(false);
@@ -628,14 +639,14 @@ test('ignores empty rules', async () => {
 
 test('handles dot notation names', async () => {
   const v = new Validator();
-  v.attach('example.name', 'required|alpha');
+  v.attach({ name: 'example.name', rules: 'required|alpha' });
   expect(await v.validate('example.name', '')).toBe(false);
   expect(await v.validate('example.name', 'ad')).toBe(true);
 });
 
 test('validations can be paused and resumed', async () => {
   const v = new Validator();
-  v.attach('name', 'required');
+  v.attach({ name: 'name', rules: 'required' });
 
   v.pause();
 
@@ -661,20 +672,20 @@ test('it can hold multiple errors for one field', async () => {
 
 test('it can set flags for attached fields', () => {
   const v = new Validator();
-  let field = v.attach('name', 'alpha');
+  let field = v.attach({ name: 'name', rules: 'alpha' });
   expect(field.flags.untouched).toBe(true);
 
   v.flag('name', { untouched: false });
   expect(field.flags.untouched).toBe(false);
 
   // scoped fields
-  v.attach('email', 'email', { scope: 'myscope' });
+  v.attach({ name: 'email', rules: 'email', scope: 'myscope' });
   expect(v.flags.$myscope.email.untouched).toBe(true);
   v.flag('myscope.email', { untouched: false });
   expect(v.flags.$myscope.email.untouched).toBe(false);
 
   // dotted name fields
-  field = v.attach('form.title', 'alpha');
+  field = v.attach({ name: 'form.title', rules: 'alpha' });
   expect(field.flags.untouched).toBe(true);
   v.flag('form.title', { untouched: false });
   expect(field.flags.untouched).toBe(false);
@@ -685,7 +696,8 @@ test('it can set flags for attached fields', () => {
   }).not.toThrow();
 
   // calls update classes.
-  field = v.attach(new Field(document.createElement('input'), {
+  field = v.attach(new Field({
+    el: document.createElement('input'),
     name: 'somefield',
     rules: 'alpha',
     classes: true
@@ -696,34 +708,37 @@ test('it can set flags for attached fields', () => {
   expect(field.updateClasses).toHaveBeenCalled();
 });
 
-describe('resets fields', () => {
-  test('resets fields matching the matcher options', () => {
-    const v = new Validator();
-    v.attach({ name: 'field' });
-    v.attach({ name: 'fieldTwo', scope: 's1' });
-    v.attach({ name: 'fieldThree', scope: 's1' });
-
-    v.errors.add({ field: 'field', msg: 'oops' });
-    v.errors.add({ field: 'fieldTwo', msg: 'oops', scope: 's1' });
-    v.errors.add({ field: 'fieldThree', msg: 'oops', scope: 's1' });
+test('resets fields matching the matcher options', async () => {
+  const v = new Validator();
+  v.attach({ name: 'field' });
+  v.attach({ name: 'fieldTwo', scope: 's1' });
+  v.attach({ name: 'fieldThree', scope: 's1' });
 
-    v.reset({ name: 'field' });
-    expect(v.errors.count()).toBe(2);
-    v.reset({ scope: 's1' });
-    expect(v.errors.count()).toBe(0);
-  })
+  v.errors.add({ field: 'field', msg: 'oops' });
+  v.errors.add({ field: 'fieldTwo', msg: 'oops', scope: 's1' });
+  v.errors.add({ field: 'fieldThree', msg: 'oops', scope: 's1' });
 
-  test('resets fields on nextTick if vm is provided', async () => {
-    const vm = { $nextTick: jest.fn(cb => cb()) };
-    const v = new Validator(null, { vm });
-    v.attach({ name: 'field' });
-    v.errors.add({ name: 'field', msg: 'oops' });
-    await v.reset();
-    expect(vm.$nextTick).toHaveBeenCalled();
-    expect(v.errors.count()).toBe(0);
-  });
+  await v.reset({ name: 'field' });
+  expect(v.errors.count()).toBe(2);
+  await v.reset({ scope: 's1' });
+  expect(v.errors.count()).toBe(0);
 });
 
+test('resets all fields', async () => {
+  const v = new Validator();
+  v.attach({ name: 'field' });
+  v.attach({ name: 'fieldTwo', scope: 's1' });
+  v.attach({ name: 'fieldThree', scope: 's1' });
+
+  v.errors.add({ field: 'field', msg: 'oops' });
+  v.errors.add({ field: 'fieldTwo', msg: 'oops', scope: 's1' });
+  v.errors.add({ field: 'fieldThree', msg: 'oops', scope: 's1' });
+
+  expect(v.errors.count()).toBe(3);
+  await v.reset();
+  expect(v.errors.count()).toBe(0);
+})
+
 test('it can handle mixed successes and errors from one field regardless of rules order', async () => {
   const v = new Validator({
     string1: 'alpha|min:3',
@@ -754,15 +769,15 @@ test('exposes static and instance readonly rules properties', () => {
 
 test('validate can resolve the value if it was not provided', async () => {
   const v = new Validator();
-  const field = v.attach('field', 'alpha', { getter: () => '123' });
+  const field = v.attach({ name: 'field', rules: 'alpha', getter: () => '123' });
   expect(await v.validate(`#${field.id}`)).toBe(false);
 });
 
 test('resolves a field by name and scope', async () => {
   const v = new Validator();
-  let field = v.attach('field', 'alpha', { scope: 's1' });
+  let field = v.attach({ rules: 'alpha', name: 'field', scope: 's1' });
   expect(v._resolveField('field', 's1')).toBe(field);
-  field = v.attach('field', 'alpha', { scope: 0 });
+  field = v.attach({ rules: 'alpha', name: 'field', scope: 0 });
   expect(v._resolveField('field', 0)).toBe(field);
 });
 
@@ -774,7 +789,7 @@ test('handles unresolved fields when strict is false by returning true', async (
 
 test('updates classes after validating a field', async () => {
   const v = new Validator();
-  const field = v.attach('field', 'alpha', { el: document.createElement('input'), classes: true });
+  const field = v.attach({ name: 'field', rules: 'alpha', el: document.createElement('input'), classes: true });
   field.updateClasses = jest.fn();
   expect(await v.validate(`#${field.id}`, '123')).toBe(false);
   expect(field.updateClasses).toHaveBeenCalled();
@@ -783,7 +798,7 @@ test('updates classes after validating a field', async () => {
 test('triggers initial validation for fields', async () => {
   const v = new Validator();
   v.validate = jest.fn();
-  const field = v.attach('field', 'alpha', { el: document.createElement('input'), getter: () => '123', initial: true });
+  const field = v.attach({ name: 'field', rules: 'alpha', el: document.createElement('input'), getter: () => '123', initial: true });
   expect(v.validate).toHaveBeenCalledWith(`#${field.id}`, '123');
 });
 
@@ -798,7 +813,7 @@ test('validates multi-valued promises', async () => {
   });
 
   const v = new Validator();
-  v.attach('field', 'many_promise');
+  v.attach({ name: 'field', rules: 'many_promise' });
   expect(await v.validate('field', 'sdad')).toBe(true);
 });