Repository: baianat/vee-validate
Pull request #1318 (2018-05-14 02:43:21)

breaking: deprecate selectors in rules in favor of refs
This PR introduces a __BREAKING CHANGE__ related to targeting other fields.

#### Overview

All targets must be a ref. deprecating the use of class, id and name selectors.

#### What did break?

- This affects the __confirmed__, __before__, and __after__ rules.
- The `confirmed` rule will no longer fallback to `${field}_confirmation` name targeting if none provided.

#### Benefits

- Removes the confusion around the different targeting selector formats since it leaves one universal way to target other fields regardless of their nature.
- SSR friendly since it removes the need for __document__ and __window__ objects. 

#### Example

Now you should always now have a ref on the target field:

```html
<input type="password" name="password" v-validate="'required|confirmed:password'">
<input type="password" name="confirmation" ref="password">
```

codecov[bot]: # [Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1318?src=pr&el=h1) Report
> Merging [#1318](https://codecov.io/gh/baianat/vee-validate/pull/1318?src=pr&el=desc) into [master](https://codecov.io/gh/baianat/vee-validate/commit/94200749435cd3aedeb9a698cf31b6941663546b?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1318      +/-   ##
==========================================
- Coverage   99.32%   99.32%   -0.01%     
==========================================
  Files          50       50              
  Lines        1492     1482      -10     
  Branches      473      470       -3     
==========================================
- Hits         1482     1472      -10     
  Misses         10       10
```


| [Impacted Files](https://codecov.io/gh/baianat/vee-validate/pull/1318?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/rules/confirmed.js](https://codecov.io/gh/baianat/vee-validate/pull/1318/diff?src=pr&el=tree#diff-c3JjL3J1bGVzL2NvbmZpcm1lZC5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [src/core/field.js](https://codecov.io/gh/baianat/vee-validate/pull/1318/diff?src=pr&el=tree#diff-c3JjL2NvcmUvZmllbGQuanM=) | `99.28% <100%> (-0.03%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1318?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/1318?src=pr&el=footer). Last update [9420074...bd7be1e](https://codecov.io/gh/baianat/vee-validate/pull/1318?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 48c68dee..11618fc7 100644
--- a/src/core/field.js
+++ b/src/core/field.js
@@ -319,12 +319,7 @@ export default class Field {
     // we get the selectors for each field.
     const fields = Object.keys(this.rules).reduce((prev, r) => {
       if (Validator.isTargetRule(r)) {
-        let selector = this.rules[r][0];
-        if (r === 'confirmed' && !selector) {
-          selector = `${this.name}_confirmation`;
-        }
-
-        prev.push({ selector, name: r });
+        prev.push({ selector: this.rules[r][0], name: r });
       }
 
       return prev;
@@ -334,28 +329,8 @@ export default class Field {
 
     // must be contained within the same component, so we use the vm root element constrain our dom search.
     fields.forEach(({ selector, name }) => {
-      let el = null;
-      // vue ref selector.
-      if (selector[0] === '$') {
-        const ref = this.vm.$refs[selector.slice(1)];
-        el = Array.isArray(ref) ? ref[0] : ref;
-      } else {
-        try {
-          // try query selector
-          el = this.vm.$el.querySelector(selector);
-        } catch (err) {
-          el = null;
-        }
-      }
-
-      if (!el) {
-        try {
-          el = this.vm.$el.querySelector(`input[name="${selector}"]`);
-        } catch (err) {
-          el = null;
-        }
-      }
-
+      const ref = this.vm.$refs[selector];
+      const el = Array.isArray(ref) ? ref[0] : ref;
       if (!el) {
         return;
       }


Test patch:
diff --git a/tests/integration/components/Targets.vue b/tests/integration/components/Targets.vue
index fa89ecea..fc388534 100644
--- a/tests/integration/components/Targets.vue
+++ b/tests/integration/components/Targets.vue
@@ -1,30 +1,19 @@
 <template>
   <div>
-    <input type="text" name="f1" v-validate="'required|confirmed:f2'" id="f1">
-    <input type="text" name="f2" id="f2">
+    <input type="text" name="f1" v-validate="'required|confirmed:text'" id="f1">
+    <input type="text" name="f2" ref="text" id="f2">
 
-    <input type="text" name="f3" v-validate="'required|confirmed:#f4'" id="f3">
+    <!-- NON EXISTANT REF -->
+    <input type="text" name="f3" v-validate="'required|confirmed:na'" id="f3">
     <input type="text" name="f4" id="f4">
 
-    <input type="text" name="f5" v-validate="'required|confirmed:.f6'" id="f5">
-    <input type="text" name="f6" class="f6">
-
-    <custom-input some-prop="f7" v-validate="'required|confirmed:$f8'" v-model="d1"></custom-input>
-    <custom-input some-prop="f8" ref="f8" v-model="d2"></custom-input>
+    <custom-input some-prop="f5" v-validate="'required|confirmed:f6'" v-model="d1"></custom-input>
+    <custom-input some-prop="f6" ref="f6" v-model="d2"></custom-input>
 
     <template v-for="n in 3">
-      <custom-input :some-prop="'f7_' + n" v-validate="'required|confirmed:$f8_' + n" v-model="d3"></custom-input>
+      <custom-input :some-prop="'f7_' + n" v-validate="'required|confirmed:f8_' + n" v-model="d3"></custom-input>
       <custom-input :some-prop="'f8_' + n" :ref="'f8_' + n" v-model="d4"></custom-input>
     </template>
-
-
-    <input type="text" name="f9" v-validate="'required|confirmed:#f10'" id="f9">
-
-    <input type="text" name="f11" v-validate="'required|confirmed:%$selector'" id="f11">
-
-    <input type="text" name="f12" v-validate="'required|confirmed'" id="f12">
-    <input type="text" name="f12_confirmation" id="f13">
-    
   </div>
 </template>
 
diff --git a/tests/integration/target.js b/tests/integration/target.js
index 73a22398..ae4e14c7 100644
--- a/tests/integration/target.js
+++ b/tests/integration/target.js
@@ -7,7 +7,7 @@ import TestComponent from './components/Targets';
 const Vue = createLocalVue();
 Vue.use(VeeValidate);
 
-test('native HTML elements targeting via name selector', async () => {
+test('native HTML elements targeting', async () => {
   const wrapper = mount(TestComponent, { localVue: Vue });
   let input = wrapper.find('#f1');
   let target = wrapper.find('#f2');
@@ -27,47 +27,7 @@ test('native HTML elements targeting via name selector', async () => {
   expect(wrapper.vm.$validator.flags.f1.valid).toBe(true);
 });
 
-test('native HTML elements targeting via id selectors', async () => {
-  const wrapper = mount(TestComponent, { localVue: Vue });
-  let input = wrapper.find('#f3');
-  let target = wrapper.find('#f4');
-
-  input.element.value = '10';
-  input.trigger('input');
-  await flushPromises();
-
-  expect(wrapper.vm.$validator.errors.first('f3')).toBe('The f3 confirmation does not match.');
-  target.element.value = '10';
-  target.trigger('input');
-  await flushPromises();
-
-  const field = wrapper.vm.$validator.fields.find({ name: 'f3' });
-
-  expect(wrapper.vm.$validator.errors.has('f3')).toBe(false);
-  expect(wrapper.vm.$validator.flags.f3.valid).toBe(true);
-});
-
-test('native HTML elements targeting via class selectors', async () => {
-  const wrapper = mount(TestComponent, { localVue: Vue });
-  let input = wrapper.find('#f5');
-  let target = wrapper.find('.f6');
-
-  input.element.value = '10';
-  input.trigger('input');
-  await flushPromises();
-
-  expect(wrapper.vm.$validator.errors.first('f5')).toBe('The f5 confirmation does not match.');
-  target.element.value = '10';
-  target.trigger('input');
-  await flushPromises();
-
-  const field = wrapper.vm.$validator.fields.find({ name: 'f5' });
-
-  expect(wrapper.vm.$validator.errors.has('f5')).toBe(false);
-  expect(wrapper.vm.$validator.flags.f5.valid).toBe(true);
-});
-
-test('custom components targeting via $refs', async () => {
+test('custom components targeting', async () => {
   const wrapper = mount(TestComponent, { localVue: Vue });
   wrapper.setData({
     d1: '10'
@@ -75,19 +35,19 @@ test('custom components targeting via $refs', async () => {
   wrapper.findAll(InputComponent).at(0).trigger('input');
   await flushPromises();
 
-  expect(wrapper.vm.$validator.errors.first('f7')).toBe('The f7 confirmation does not match.');
+  expect(wrapper.vm.$validator.errors.first('f5')).toBe('The f5 confirmation does not match.');
   wrapper.setData({
     d2: '10'
   });
   wrapper.findAll(InputComponent).at(1).trigger('input');
   await flushPromises();
 
-  expect(wrapper.vm.$validator.errors.has('f7')).toBe(false);
-  expect(wrapper.vm.$validator.flags.f7.valid).toBe(true);
+  expect(wrapper.vm.$validator.errors.has('f5')).toBe(false);
+  expect(wrapper.vm.$validator.flags.f5.valid).toBe(true);
 });
 
 // tests #1107
-test('custom components targeting via $ref in a v-for', async () => {
+test('refs with v-for loops', async () => {
   const wrapper = mount(TestComponent, { localVue: Vue });
   wrapper.setData({
     d3: '10'
@@ -119,48 +79,9 @@ test('custom components targeting via $ref in a v-for', async () => {
 
 test('fails silently if it cannot find the target field', async () => {
   const wrapper = mount(TestComponent, { localVue: Vue });
-  const input = wrapper.find('#f9');
-  input.trigger('input');
-  await flushPromises();
-
-  expect(wrapper.vm.$validator.fields.find({ name: 'f9' }).dependencies.length).toBe(0);
-});
-
-test('catches invalid selectors', async () => {
-  const wrapper = mount(TestComponent, { localVue: Vue });
-  const input = wrapper.find('#f11');
-  input.trigger('input');
-  await flushPromises();
-
-  expect(wrapper.vm.$validator.fields.find({ name: 'f11' }).dependencies.length).toBe(0);
-});
-
-test('handles where a querySelector method is not available #870', async () => {
-  const wrapper = mount(TestComponent, { localVue: Vue });
-  const input = wrapper.find('#f11');
-  wrapper.vm.$validator.fields.find({ name: 'f11' }).vm = { $el: {} };
+  const input = wrapper.find('#f3');
   input.trigger('input');
   await flushPromises();
 
-  expect(wrapper.vm.$validator.fields.find({ name: 'f11' }).dependencies.length).toBe(0);
+  expect(wrapper.vm.$validator.fields.find({ name: 'f3' }).dependencies.length).toBe(0);
 });
-
-test('has a fallback for the confirmed rule name selector', async () => {
-  const wrapper = mount(TestComponent, { localVue: Vue });
-  let input = wrapper.find('#f12');
-  let target = wrapper.find('#f13');
-
-  input.element.value = '10';
-  input.trigger('input');
-  await flushPromises();
-
-  expect(wrapper.vm.$validator.errors.first('f12')).toBe('The f12 confirmation does not match.');
-  target.element.value = '10';
-  target.trigger('input');
-  await flushPromises();
-
-  const field = wrapper.vm.$validator.fields.find({ name: 'f12' });
-
-  expect(wrapper.vm.$validator.errors.has('f12')).toBe(false);
-  expect(wrapper.vm.$validator.flags.f12.valid).toBe(true);
-});
\ No newline at end of file
diff --git a/tests/unit/validator.js b/tests/unit/validator.js
index 8dc0b1bf..04fb338b 100644
--- a/tests/unit/validator.js
+++ b/tests/unit/validator.js
@@ -345,7 +345,10 @@ test('add rules than can target other fields', async () => {
   v.attach({
     name: 'otherField',
     vm: {
-      $el: document.body
+      $el: document.body,
+      $refs: {
+        field: el
+      }
     },
     rules: 'isBigger:field'
   });
@@ -495,7 +498,10 @@ test('can translate target field for field dependent validations', async () => {
   v.attach({
     name: 'birthday',
     vm: {
-      $el: document.body
+      $el: document.body,
+      $refs: {
+        birthday_min: el
+      }
     },
     rules: 'date_format:DD-MM-YYYY|after:birthday_min'
   });
@@ -873,6 +879,34 @@ test('triggers initial validation for fields', async () => {
   expect(v.validate).toHaveBeenCalledWith(`#${field.id}`, '123');
 });
 
+test('updates classes on related radio input fields', async () => {
+  function createRadio (val) {
+    const el = document.createElement('input');
+    el.name = 'myinput';
+    el.type = 'radio';
+    el.value = val;
+
+    document.body.appendChild(el);
+    return el;
+  }
+  const v = new Validator();
+  const el = createRadio('1');
+  const el2 = createRadio('2');
+  const el3 = createRadio('3');
+
+  const field = v.attach({ name: 'field', rules: 'required', el, classes: true });
+  expect(await v.validate(`#${field.id}`)).toBe(false);
+  expect(el.classList.contains('invalid')).toBe(true);
+  expect(el2.classList.contains('invalid')).toBe(true);
+  expect(el3.classList.contains('invalid')).toBe(true);
+
+  el.checked = true;
+  expect(await v.validate(`#${field.id}`, '1')).toBe(true);
+  expect(el.classList.contains('valid')).toBe(true);
+  expect(el2.classList.contains('valid')).toBe(true);
+  expect(el3.classList.contains('valid')).toBe(true);
+});
+
 test('validates multi-valued promises', async () => {
   Validator.extend('many_promise', () => {
     return new Promise(resolve => {
@@ -895,7 +929,10 @@ test('it should pass the after/before inclusion parameters correctly', async ()
   v.attach({
     name: 'birthday',
     vm: {
-      $el: document.body
+      $el: document.body,
+      $refs: {
+        field: el
+      }
     },
     rules: 'date_format:DD/MM/YYYY|after:field,true'
   });