Repository: baianat/vee-validate
Pull request #1958 (2019-03-23 16:03:32)

Fix typo in validator.md
🔎 __Overview__

Fix typo in validator.md

codecov[bot]: # [Codecov](https://codecov.io/gh/baianat/vee-validate/pull/1958?src=pr&el=h1) Report
> Merging [#1958](https://codecov.io/gh/baianat/vee-validate/pull/1958?src=pr&el=desc) into [master](https://codecov.io/gh/baianat/vee-validate/commit/6e62a9070e542b114dbb2a4c66dd385b37fc7728?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/1958/graphs/tree.svg?width=650&token=XFzlvHKFP5&height=150&src=pr)](https://codecov.io/gh/baianat/vee-validate/pull/1958?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1958   +/-   ##
=======================================
  Coverage   95.91%   95.91%           
=======================================
  Files          62       62           
  Lines        2326     2326           
  Branches      724      724           
=======================================
  Hits         2231     2231           
  Misses         85       85           
  Partials       10       10
```



------

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


Patch:
diff --git a/docs/api/validator.md b/docs/api/validator.md
index da22141e..50447525 100644
--- a/docs/api/validator.md
+++ b/docs/api/validator.md
@@ -16,12 +16,12 @@ The validator offers an API to add new fields and trigger validations.
 
 |Name  | Return Type  |Description  |
 |---------|---------|---------|
-| attach(field: FieldOptions) | `Field` | attaches a new field to the validator. |
+| attach(field: FieldOptions) | `Field` | Attaches a new field to the validator. |
 | validate(descriptor?: String, value?: any, options?: Object) | `Promise<boolean>` | Validates the matching fields of the provided [descriptor](#field-descriptor). when validation is done, the Promise resolves a boolean indicating whether the selected was valid or not. |
 | validateAll(fields?: String or Object) | `Promise<boolean>` | Validates each value against the corresponding field validations. |
 | pause() | `void` | Disables validation. |
 | resume() | `void` | Enables validation. |
-| verify(value: any, rules: string | Object) | { errors: string[], valid: boolean, failedRules: { [x: string]: string } } | [verify method](#verify) |
+| verify(value: any, rules: string \| Object) | { errors: string[], valid: boolean, failedRules: { [x: string]: string } } | [Verify method](#verify). |
 | detach(name: string, scope?: string) | `void` | Detaches the field that matches the name and the scope of the provided values. |
 | extend(name: string, rule: Rule, options?: ExtendOptions) | `void` | Adds a new validation rule. The provided rule param must be a [valid Rule function or object](/guide/custom-rules.md). |
 | reset(matcher?: Object) | `void` | Resets field flags for all scoped fields. Resets all fields if no scope is provided. |
diff --git a/docs/guide/components/validation-provider.md b/docs/guide/components/validation-provider.md
index fa91db4b..0e41a1f2 100644
--- a/docs/guide/components/validation-provider.md
+++ b/docs/guide/components/validation-provider.md
@@ -289,6 +289,32 @@ Ideally you would pass the props you need to either the `ValidationProvider` or
 
 Using either of these approaches is at your preference.
 
+### Persisting Provider Errors
+
+Sometimes when building something like a multi-step form, you would need to use `v-if` on your providers to toggle the visibility of your steps. However, when the provider is hidden and shown again, it does not keep its state.
+
+You can use the `persist` prop to allow the provider to __remember__ its state across mounting/destroyed lifecycles, but there are a couple of caveats:
+
+- Your Provider __must be inside__ an __observer__ component.
+- Your Provider __must have__ a `vid` property set.
+
+```vue
+<ValidationObserver>
+  <div v-if="!isHidden">
+    <ValidationProvider
+      rules="required|min:3|max:6"
+      vid="myfield"
+      v-slot="{ errors }"
+      :persist="true"
+    >
+      <input type="text" v-model="value">
+      {{ errors[0] }}
+    </ValidationProvider>
+  </div>
+</ValidationObserver>
+<button @click="isHidden = !isHidden">Toggle</button>
+```
+
 ## Adding Errors Manually
 
 You may want to add manual errors to a field, such cases like pre-filling initially due to a server response, or an async request. You can do this using `refs` and the `applyResult` method.
@@ -355,6 +381,7 @@ All the following props are optional.
 | bails     | `boolean` | `true`                | If true, the validation will stop on the first failing rule.                 |
 | debounce  | `number`  | `0`                   | Debounces the validation for the specified amount of milliseconds.           |
 | tag       | `string`  | `span`                | The default tag to [render](#rendering). |
+| persist   | `boolean` | `false`               | If true, the provider will keep its errors across mounted/destroyed lifecycles |
 
 ### Methods
 
diff --git a/src/components/observer.js b/src/components/observer.js
index f4c056d2..441c0af8 100644
--- a/src/components/observer.js
+++ b/src/components/observer.js
@@ -1,4 +1,4 @@
-import { isCallable, values, findIndex } from '../utils';
+import { isCallable, values, findIndex, warn } from '../utils';
 
 const flagMergingStrategy = {
   pristine: 'every',
@@ -115,6 +115,7 @@ export const ValidationObserver = {
   },
   render (h) {
     let slots = this.$scopedSlots.default;
+    this._persistedStore = this._persistedStore || {};
     if (!isCallable(slots)) {
       return h(this.tag, this.$slots.default);
     }
@@ -132,11 +133,13 @@ export const ValidationObserver = {
       }
 
       this.refs = Object.assign({}, this.refs, { [subscriber.vid]: subscriber });
+      if (subscriber.persist && this._persistedStore[subscriber.vid]) {
+        this.restoreProviderState(subscriber);
+      }
     },
     unsubscribe ({ vid }, kind = 'provider') {
       if (kind === 'provider') {
-        this.$delete(this.refs, vid);
-        return;
+        this.removeProvider(vid);
       }
 
       const idx = findIndex(this.observers, o => o.vid === vid);
@@ -152,6 +155,32 @@ export const ValidationObserver = {
     },
     reset () {
       return [...values(this.refs), ...this.observers].forEach(ref => ref.reset());
-    }
+    },
+    restoreProviderState (provider) {
+      const state = this._persistedStore[provider.vid];
+      provider.setFlags(state.flags);
+      provider.applyResult(state);
+      delete this._persistedStore[provider.vid];
+    },
+    removeProvider (vid) {
+      const provider = this.refs[vid];
+      // save it for the next time.
+      if (provider && provider.persist) {
+        /* istanbul ignore else */
+        if (process.env.NODE_ENV !== 'production') {
+          if (vid.indexOf('_vee_') === 0) {
+            warn('Please provide a `vid` prop when using `persist`, there might be unexpected issues otherwise.');
+          }
+        }
+
+        this._persistedStore[vid] = {
+          flags: provider.flags,
+          errors: provider.messages,
+          failedRules: provider.failedRules
+        };
+      }
+
+      this.$delete(this.refs, vid);
+    },
   }
 };
diff --git a/src/components/provider.js b/src/components/provider.js
index 9a42dd3c..5771b630 100644
--- a/src/components/provider.js
+++ b/src/components/provider.js
@@ -220,7 +220,7 @@ export const ValidationProvider = {
       default: () => {
         PROVIDER_COUNTER++;
 
-        return PROVIDER_COUNTER;
+        return `_vee_${PROVIDER_COUNTER}`;
       }
     },
     name: {
@@ -307,7 +307,7 @@ export const ValidationProvider = {
       return Object.keys(rules).filter(RuleContainer.isTargetRule).map(rule => {
         const depName = rules[rule][0];
         const watcherName = `$__${depName}`;
-        if (!isCallable(this[watcherName])) {
+        if (!isCallable(this[watcherName]) && providers[depName]) {
           this[watcherName] = providers[depName].$watch('value', () => {
             if (this.flags.validated) {
               this._needsValidation = true;


Test patch:
diff --git a/tests/unit/component.js b/tests/unit/component.js
index 55c0568d..cd08f8bf 100644
--- a/tests/unit/component.js
+++ b/tests/unit/component.js
@@ -864,4 +864,45 @@ describe('Validation Observer Component', () => {
     expect(errors.at(0).text()).toBe(DEFAULT_REQUIRED_MESSAGE);
     expect(errors.at(1).text()).toBe(DEFAULT_REQUIRED_MESSAGE);
   });
+
+  test('persist provider state after destroyed', async () => {
+    const wrapper = mount({
+      data: () => ({
+        value: '',
+        isHidden: false
+      }),
+      template: `
+      <div>
+        <ValidationObserver>
+          <div v-if="!isHidden">
+            <ValidationProvider
+              rules="required|min:3|max:6"
+              vid="myfield"
+              v-slot="{ errors }"
+              :persist="true"
+            >
+              <input type="text" v-model="value">
+              <span>{{ errors[0] }}</span>
+            </ValidationProvider>
+          </div>
+        </ValidationObserver>
+        <button @click="isHidden = !isHidden">Toggle</button>
+      </div>
+      `
+    }, { localVue: Vue });
+
+    const button = wrapper.find('button');
+    const input = wrapper.find('input');
+    await flushPromises();
+    input.element.value = 'se';
+    input.trigger('input');
+    await flushPromises();
+
+    button.trigger('click');
+    await flushPromises();
+    button.trigger('click');
+    await flushPromises();
+    const span = wrapper.find('span');
+    expect(span.text()).toBeTruthy();
+  });
 });