Repository: baianat/vee-validate
Pull request #1802 (2019-01-15 12:38:07)

Fixed typo in conditional and looping guide
Specifically in the `v-if` section.

🔎 __Overview__

This PR fixes a typo in the Conditional and Looping guide within the docs.

✔ __Issues affected__

No issue impacted


codecov[bot]: # [Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1802?src=pr&el=h1) Report
> Merging [#1802](https://codecov.io/gh/baianat/vee-validate/pull/1802?src=pr&el=desc) into [master](https://codecov.io/gh/baianat/vee-validate/commit/b9210749169c10c5cd3050b94f67031d97292da7?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@           Coverage Diff           @@
##           master    #1802   +/-   ##
=======================================
  Coverage   97.47%   97.47%           
=======================================
  Files          57       57           
  Lines        2138     2138           
  Branches      667      667           
=======================================
  Hits         2084     2084           
  Misses         48       48           
  Partials        6        6
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1802?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/1802?src=pr&el=footer). Last update [b921074...dc5067f](https://codecov.io/gh/baianat/vee-validate/pull/1802?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/api/directive.md b/docs/api/directive.md
index 9cb7cbe7..844d92aa 100644
--- a/docs/api/directive.md
+++ b/docs/api/directive.md
@@ -65,6 +65,41 @@ export default {
 };
 </script>
 ```
+### persist
+
+You can use `.persist` modifier to keep the field (and its possible error(s)) in memory when the field is unmount/removed from the DOM (e.g. in a `v-if` block).
+If the field disappears from your component, it will stay accessible in the `fields` object.
+You'll be able to run a `validateAll()` with all your disappeared fields, very handy if you want to develop a multi-steps form with switching components.
+
+```vue
+<template>
+  <div>
+    <div v-if="displayField">
+      <input v-model="email" v-validate.persist="'required|email'" data-vv-validate-on="input" name="email" type="text">
+    </div>
+    <label><input type="checkbox" v-model="displayField"> Display the field</label>
+    <div>{{ errors.first('email') }}</div>
+  </div>
+</template>
+
+<script>
+export default {
+  data: () => ({
+    email: '',
+    displayField: true
+  })
+};
+</script>
+```
+
+::: tip
+You can force it to disappear completely by changing the `persist` property in the field object:
+```js
+this.$validator.fields.find({name: 'email'}).persist = false;
+this.diplayField = false;
+// The field instance and its errors will also disappear from the memory in the next tick
+```
+:::
 
 ### continues
 
diff --git a/docs/guide/conditional-and-looping-inputs.md b/docs/guide/conditional-and-looping-inputs.md
index 0b79877f..4d30176c 100644
--- a/docs/guide/conditional-and-looping-inputs.md
+++ b/docs/guide/conditional-and-looping-inputs.md
@@ -21,7 +21,7 @@ The official example uses the following template:
 
 ## Handling v-if
 
-When attempting the validate those inputs, it would seem that vee-validate thinks they are the same one depending on who got rendered initially. This is due to Vue actually re-using the input, so to `vee-validate` and due to some limitation in the `directive` API, vee-validate doesn't know that the input got switched. So you need to help it a little bit by using `key` attribute on the inputs which forces Vue to render those elements independently.
+When attempting to validate those inputs, it would seem that vee-validate thinks they are the same one depending on who got rendered initially. This is due to Vue actually re-using the input, so to `vee-validate` and due to some limitation in the `directive` API, vee-validate doesn't know that the input got switched. So you need to help it a little bit by using `key` attribute on the inputs which forces Vue to render those elements independently.
 
 ```html
 <template v-if="loginType === 'username'">
diff --git a/src/components/provider.js b/src/components/provider.js
index 50216486..477ba8a9 100644
--- a/src/components/provider.js
+++ b/src/components/provider.js
@@ -187,6 +187,10 @@ export const ValidationProvider = {
       type: Boolean,
       default: false
     },
+    persist: {
+      type: Boolean,
+      default: false
+    },
     bails: {
       type: Boolean,
       default: () => VeeValidate.config.fastExit
diff --git a/src/core/field.js b/src/core/field.js
index 19996ffd..c394222f 100644
--- a/src/core/field.js
+++ b/src/core/field.js
@@ -27,6 +27,7 @@ import {
 const DEFAULT_OPTIONS = {
   targetOf: null,
   immediate: false,
+  persist: false,
   scope: null,
   listen: true,
   name: null,
@@ -92,7 +93,7 @@ export default class Field {
     this._delay = !isNullOrUndefined(options.delay) ? options.delay : 0; // cache initial delay
     this.validity = options.validity;
     this.aria = options.aria;
-    this.flags = createFlags();
+    this.flags = options.flags || createFlags();
     this.vm = options.vm;
     this.componentInstance = options.component;
     this.ctorConfig = this.componentInstance ? getPath('$options.$_veeValidate', this.componentInstance) : undefined;
@@ -232,6 +233,7 @@ export default class Field {
   update (options: Object) {
     this.targetOf = options.targetOf || null;
     this.immediate = options.immediate || this.immediate || false;
+    this.persist = options.persist || this.persist || false;
 
     // update errors scope if the field scope was changed.
     if (!isNullOrUndefined(options.scope) && options.scope !== this.scope && isCallable(this.validator.update)) {
diff --git a/src/core/resolver.js b/src/core/resolver.js
index b842c0e9..044cb6d0 100644
--- a/src/core/resolver.js
+++ b/src/core/resolver.js
@@ -42,6 +42,7 @@ export default class Resolver {
       delay: Resolver.resolveDelay(el, vnode, options),
       rules: Resolver.resolveRules(el, binding, vnode),
       immediate: !!binding.modifiers.initial || !!binding.modifiers.immediate,
+      persist: !!binding.modifiers.persist,
       validity: options.validity,
       aria: options.aria,
       initialValue: Resolver.resolveInitialValue(vnode)
diff --git a/src/core/validator.js b/src/core/validator.js
index 6496d190..da38865e 100644
--- a/src/core/validator.js
+++ b/src/core/validator.js
@@ -164,6 +164,16 @@ export default class Validator {
    * Registers a field to be validated.
    */
   attach (fieldOpts: FieldOptions): Field {
+    // We search for a field with the same name, having persist enabled
+    const oldField = fieldOpts.persist ? this.fields.find({ name: fieldOpts.name, persist: true }) : null;
+
+    if (oldField) {
+      // We keep the flags of the old field, then we remove its instance
+      fieldOpts.flags = oldField.flags;
+      oldField.destroy();
+      this.fields.remove(oldField);
+    }
+
     // fixes initial value detection with v-model and select elements.
     const value = fieldOpts.initialValue;
     const field = new Field(fieldOpts);
@@ -201,9 +211,12 @@ export default class Validator {
     let field = isCallable(name.destroy) ? name : this._resolveField(name, scope, uid);
     if (!field) return;
 
-    field.destroy();
-    this.errors.remove(field.name, field.scope, field.vmId);
-    this.fields.remove(field);
+    // We destroy/remove the field & error instances if it's not a `persist` one
+    if (!field.persist) {
+      field.destroy();
+      this.errors.remove(field.name, field.scope, field.vmId);
+      this.fields.remove(field);
+    }
   }
 
   /**


Test patch:
diff --git a/tests/integration/components/Persist.vue b/tests/integration/components/Persist.vue
new file mode 100644
index 00000000..3646d25d
--- /dev/null
+++ b/tests/integration/components/Persist.vue
@@ -0,0 +1,19 @@
+<template>
+  <div v-if="displayFields">
+    <input type="text" name="field" v-validate="'required'">
+    <input type="text" name="field-immediate" v-validate.immediate="'required'">
+    <input type="text" name="field-persist" v-validate.persist="'required'">
+    <input type="text" name="field-persist-immediate" v-validate.persist.immediate="'required'">
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'persist-test',
+  data () {
+    return {
+      displayFields: true
+    };
+  }
+};
+</script>
diff --git a/tests/integration/persist.js b/tests/integration/persist.js
new file mode 100644
index 00000000..99b80adc
--- /dev/null
+++ b/tests/integration/persist.js
@@ -0,0 +1,57 @@
+import { shallow, createLocalVue } from '@vue/test-utils';
+import flushPromises from 'flush-promises';
+import VeeValidate from '@/index';
+import TestComponent from './components/Persist';
+
+const Vue = createLocalVue();
+Vue.use(VeeValidate);
+
+test('persist fields should stay in the field list, as well as their errors', async () => {
+  const wrapper = shallow(TestComponent, { localVue: Vue });
+
+  await flushPromises();
+
+  // await new Promise(resolve => wrapper.vm.$nextTick(() => resolve()));
+  expect(wrapper.vm.errors.count()).toBe(2);
+  expect(Object.keys(wrapper.vm.fields).length).toBe(4);
+
+  wrapper.vm.displayFields = false; // We hide all the fields
+  await flushPromises();
+
+  expect(wrapper.vm.errors.count()).toBe(1); // Only one persist field is errored (because one is immediate)
+  expect(Object.keys(wrapper.vm.fields).length).toBe(2); // We expect the persist fields to be there
+
+  wrapper.vm.displayFields = true;
+  await flushPromises();
+
+  expect(wrapper.vm.errors.count()).toBe(2);
+  expect(Object.keys(wrapper.vm.fields).length).toBe(4); // The four fields are visible
+
+  expect(await wrapper.vm.$validator.validateAll()).toBe(false); // 4 required -> 4 errors
+
+  expect(wrapper.vm.errors.count()).toBe(4); // The .validateAll call has triggered 4 errors
+  expect(Object.keys(wrapper.vm.fields).length).toBe(4);
+
+  wrapper.vm.displayFields = false;
+  await flushPromises();
+
+  expect(wrapper.vm.errors.count()).toBe(2); // The two persist fields are still errored
+  expect(Object.keys(wrapper.vm.fields).length).toBe(2);
+
+  wrapper.vm.displayFields = true;
+  await flushPromises();
+
+  expect(wrapper.vm.errors.count()).toBe(3); // The two persist fields are still errored
+  expect(Object.keys(wrapper.vm.fields).length).toBe(4);
+
+  // The `valid` flag must stay the same as before
+  expect(wrapper.vm.fields['field-persist'].valid).toBe(false);
+
+  // One of the ways to remove the field is to set the persist prop to false
+  wrapper.vm.$validator.fields.find({ name: 'field-persist' }).persist = false;
+  wrapper.vm.displayFields = false;
+  await flushPromises();
+
+  expect(wrapper.vm.errors.count()).toBe(1);
+  expect(Object.keys(wrapper.vm.fields).length).toBe(1);
+});