Repository: baianat/vee-validate (js)

Issue:
ValidationProvider `persist` behavior is different than the directive
Reported in #1961, the `persist` behavior for the Provider doesn't work quite the same as the directive. Both restore the errors to the field just fine when it's re-mounted.

In the directives, errors are visible even after unmounting.

In the observer context, errors are not visible after unmounting as they are stored internally by non-reactive storage.

The Observer context should "keep" the errors visible indefinitely.

Pull request description:
[fix] `Observer` should have errors for persisted fields
🔎 __Overview__

The `persist` prop behavior is not the same as the directive one, the errors and validations state should still be observed inside the observer slot props.

✔ __Issues affected__

closes #2089 


Patch:
diff --git a/src/components/observer.js b/src/components/observer.js
index af4ca22e..04d2dd93 100644
--- a/src/components/observer.js
+++ b/src/components/observer.js
@@ -1,4 +1,4 @@
-import { isCallable, values, findIndex, warn } from '../utils';
+import { isCallable, values, findIndex, warn, assign } from '../utils';
 import { createRenderless } from '../utils/vnode';
 
 const flagMergingStrategy = {
@@ -53,6 +53,7 @@ export const ValidationObserver = {
     vid: `obs_${OBSERVER_COUNTER++}`,
     refs: {},
     observers: [],
+    persistedStore: {}
   }),
   computed: {
     ctx () {
@@ -78,6 +79,13 @@ export const ValidationObserver = {
 
       return [
         ...values(this.refs),
+        ...Object.keys(this.persistedStore).map(key => {
+          return {
+            vid: key,
+            flags: this.persistedStore[key].flags,
+            messages: this.persistedStore[key].errors
+          };
+        }),
         ...this.observers,
       ].reduce((acc, provider) => {
         Object.keys(flagMergingStrategy).forEach(flag => {
@@ -124,8 +132,6 @@ export const ValidationObserver = {
       slots = slots(this.ctx);
     }
 
-    this._persistedStore = this._persistedStore || {};
-
     return this.slim ? createRenderless(h, slots) : h(this.tag, { on: this.$listeners, attrs: this.$attrs }, slots);
   },
   methods: {
@@ -136,7 +142,7 @@ export const ValidationObserver = {
       }
 
       this.refs = Object.assign({}, this.refs, { [subscriber.vid]: subscriber });
-      if (subscriber.persist && this._persistedStore[subscriber.vid]) {
+      if (subscriber.persist && this.persistedStore[subscriber.vid]) {
         this.restoreProviderState(subscriber);
       }
     },
@@ -157,13 +163,16 @@ export const ValidationObserver = {
       ]).then(results => results.every(r => r));
     },
     reset () {
+      Object.keys(this.persistedStore).forEach((key) => {
+        this.$delete(this.persistedStore, key);
+      });
       return [...values(this.refs), ...this.observers].forEach(ref => ref.reset());
     },
     restoreProviderState (provider) {
-      const state = this._persistedStore[provider.vid];
+      const state = this.persistedStore[provider.vid];
       provider.setFlags(state.flags);
       provider.applyResult(state);
-      delete this._persistedStore[provider.vid];
+      this.$delete(this.persistedStore, provider.vid);
     },
     removeProvider (vid) {
       const provider = this.refs[vid];
@@ -176,11 +185,13 @@ export const ValidationObserver = {
           }
         }
 
-        this._persistedStore[vid] = {
-          flags: provider.flags,
-          errors: provider.messages,
-          failedRules: provider.failedRules
-        };
+        this.persistedStore = assign({}, this.persistedStore, {
+          [vid]: {
+            flags: provider.flags,
+            errors: provider.messages,
+            failedRules: provider.failedRules
+          }
+        });
       }
 
       this.$delete(this.refs, vid);


Test patch:
diff --git a/tests/unit/component.js b/tests/unit/component.js
index da1580f3..af80a008 100644
--- a/tests/unit/component.js
+++ b/tests/unit/component.js
@@ -898,7 +898,7 @@ describe('Validation Observer Component', () => {
       }),
       template: `
       <div>
-        <ValidationObserver>
+        <ValidationObserver v-slot="form">
           <div v-if="!isHidden">
             <ValidationProvider
               rules="required|min:3|max:6"
@@ -907,9 +907,10 @@ describe('Validation Observer Component', () => {
               :persist="true"
             >
               <input type="text" v-model="value">
-              <span>{{ errors[0] }}</span>
+              <span class="error-message">{{ errors[0] }}</span>
             </ValidationProvider>
           </div>
+          <span class="error-message">{{ form.errors.myfield && form.errors.myfield[0] }}</span>
         </ValidationObserver>
         <button @click="isHidden = !isHidden">Toggle</button>
       </div>
@@ -925,9 +926,14 @@ describe('Validation Observer Component', () => {
 
     button.trigger('click');
     await flushPromises();
+    // should be hidden, but the errors should still be visible in the observer.
+    expect(wrapper.find('.error-message').text()).toBeTruthy();
+
     button.trigger('click');
     await flushPromises();
-    const span = wrapper.find('span');
-    expect(span.text()).toBeTruthy();
+    const span = wrapper.findAll('.error-message');
+    // both should have the same error message, meaning the errors is restored to the provider.
+    expect(span.at(0).text()).toBe('The {field} field must be at least 3 characters.');
+    expect(span.at(1).text()).toBe('The {field} field must be at least 3 characters.');
   });
 });