Skip to content

Commit

Permalink
fix(core): reset the $externalResults key for a changed property, closes
Browse files Browse the repository at this point in the history
 #891 (#916)

* fix: reset the externalResults key for a changed property

* docs: make it clear how we reset $externalResults
  • Loading branch information
dobromir-hristov committed Aug 14, 2021
1 parent 13787d8 commit 67d56d7
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 87 deletions.
38 changes: 24 additions & 14 deletions packages/docs/src/advanced_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,28 +518,18 @@ async function validate () {
await doAsyncStuff()
// do server validation, and assume we have these errors
const errors = {
// foo: 'error', is also supported
foo: ['Error one', 'Error Two']
}
// add the errors into the external results object
$externalResults.value = errors // if using a `reactive` object instead, use `Object.assign($externalResults, errors)`
$externalResults.value = errors
// if using a `reactive` object instead,
// Object.assign($externalResults, errors)
}

return { v, validate }
```

To clear out the external results, you should use the handy `$clearExternalResults()` method, that Vuelidate provides. It will properly handle
both `ref` and `reactive` objects.

```js
async function validate () {
// clear out old external results
v.value.$clearExternalResults()
// check if everything is valid
if (!await v.value.$validate()) return
//
}
```

### External results with Options API

When using the Options API, you just need to define a `vuelidateExternalResults` data property, and assign the errors to it.
Expand Down Expand Up @@ -582,6 +572,26 @@ async function validate () {
}
```

### Clearing $externalResults

If you are using `$model` to modify your form state, Vuelidate automatically will clear any corresponding external results.

If you are using `$autoDirty: true`, then Vuelidate will track any changes to your form state and reset the external results as well, no need to
use `$model`

If you need to clear the entire object, use the handy `$clearExternalResults()` method, that Vuelidate provides. It will properly handle both `ref`
and `reactive` objects.

```js
async function validate () {
// clear out old external results
v.value.$clearExternalResults()
// check if everything is valid
if (!await v.value.$validate()) return
//
}
```

## i18n support

Validator messages are very flexible. You can wrap each validator with a helper, that returns a translated error message, based on the validator name.
Expand Down
1 change: 1 addition & 0 deletions packages/test-project/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<li><router-link to="/nested-validations">Nested Validations</router-link></li>
<li><router-link to="/nested-ref">Nested Ref</router-link></li>
<li><router-link to="/collection-validations">Collection Validations</router-link></li>
<li><router-link to="/external-validations">External Validations</router-link></li>
</ul>
</div>
<router-view />
Expand Down
85 changes: 85 additions & 0 deletions packages/test-project/src/components/ExternalValidationsForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<template>
<div class="SimpleForm">
<label>name</label>
<input
v-model="name"
type="text"
>
<label>twitter</label>
<input
v-model="social.twitter"
type="text"
>
<label>github</label>
<input
v-model="social.github"
type="text"
>
<button @click="validate">
Validate
</button>
<button @click="v$.$touch">
$touch
</button>
<button @click="v$.$reset">
$reset
</button>
<div style="background: rgba(219, 53, 53, 0.62); color: #ff9090; padding: 10px 15px">
<p
v-for="(error, index) of v$.$errors"
:key="index"
style="padding: 0; margin: 5px 0"
>
{{ error.$message }}
</p>
</div>
<pre>{{ v$ }}</pre>
</div>
</template>

<script>
import { ref, reactive } from 'vue'
import useVuelidate from '@vuelidate/core'
import { required, helpers, minLength } from '@vuelidate/validators'
export default {
name: 'ExternalValidationsForm',
setup () {
const name = ref('given name')
const social = reactive({
github: 'hi',
twitter: 'hey'
})
const $externalResults = reactive({
name: '',
social: {
github: '',
twitter: ''
}
})
let v$ = useVuelidate(
{
name: {
required: helpers.withMessage('This field is required', required)
},
social: {
github: { minLength: minLength(5) },
twitter: { minLength: minLength(5) }
}
}, { name, social }, { $autoDirty: true, $externalResults })
return { name, v$, social, external: $externalResults }
},
methods: {
validate () {
this.v$.$validate().then((result) => {
console.log('Result is', result)
this.external.name = 'Name External'
this.external.social.github = 'Github External'
})
}
}
}
</script>
5 changes: 5 additions & 0 deletions packages/test-project/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import OldApiExample from './components/OldApiExample.vue'
import ChainOfRefs from './components/ChainOfRefs.vue'
import CollectionValidations from './components/CollectionValidations.vue'
import I18nSimpleForm from './components/I18nSimpleForm.vue'
import ExternalValidationsForm from './components/ExternalValidationsForm.vue'

export const routes = [
{
Expand All @@ -29,5 +30,9 @@ export const routes = [
{
path: '/collection-validations',
component: CollectionValidations
},
{
path: '/external-validations',
component: ExternalValidationsForm
}
]
16 changes: 10 additions & 6 deletions packages/vuelidate/src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ function createValidationResults (rules, model, key, resultsCache, path, config,
$propertyPath: path,
$property: key,
$validator: '$externalResults',
$uid: `${path}-${index}`,
$uid: `${path}-externalResult-${index}`,
$message: stringError,
$params: {},
$response: null,
Expand Down Expand Up @@ -644,6 +644,10 @@ export function setValidations ({
set: val => {
$dirty.value = true
const s = unwrap(state)
const external = unwrap(externalResults)
if (external) {
external[key] = cachedExternalResults[key]
}
if (isRef(s[key])) {
s[key].value = val
} else {
Expand All @@ -653,12 +657,12 @@ export function setValidations ({
}) : null

if (key && mergedConfig.$autoDirty) {
const $unwatch = watch(nestedState, () => {
const autoDirtyPath = `_${path}_$watcher_`
const cachedAutoDirty = resultsCache.get(autoDirtyPath, {})
watch(nestedState, () => {
if (!$dirty.value) $touch()
if (cachedAutoDirty) cachedAutoDirty.$unwatch()
resultsCache.set(autoDirtyPath, {}, { $unwatch })
const external = unwrap(externalResults)
if (external) {
external[key] = cachedExternalResults[key]
}
}, { flush: 'sync' })
}

Expand Down
71 changes: 36 additions & 35 deletions packages/vuelidate/test/unit/specs/optionsApi.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,75 +314,82 @@ describe('OptionsAPI validations', () => {
})

describe('external results', () => {
it('saves external results, by changing individual properties', async () => {
const externalErrorObject = {
$message: 'foo',
$params: {},
$pending: false,
$property: 'number',
$propertyPath: 'number',
$response: null,
$uid: 'number-externalResult-0',
$validator: '$externalResults'
}

it('saves external results, by changing individual properties, using `$model` to track changes', async () => {
const validation = {
number: { isEven }
}
const { vm } = await createOldApiSimpleWrapper(validation, { number: 1, vuelidateExternalResults: { number: '' } })
const vuelidateExternalResults = { number: '' }

const { vm } = await createOldApiSimpleWrapper(validation, { number: 1, vuelidateExternalResults })

vm.v.$touch()
expect(vm.vuelidateExternalResults).toEqual({ number: '' })
expect(vm.vuelidateExternalResults).toEqual(vuelidateExternalResults)

expect(vm.v).toHaveProperty('number', expect.any(Object))
expect(vm.v.number.$externalResults).toEqual([])
// set an external validation result
vm.vuelidateExternalResults.number = ['foo']
// assert
const externalErrorObject = {
$message: 'foo',
$params: {},
$pending: false,
$property: 'number',
$propertyPath: 'number',
$response: null,
$uid: 'number-0',
$validator: '$externalResults'
}
expect(vm.v.number.$externalResults).toEqual([externalErrorObject])
expect(vm.v.number.$error).toBe(true)
expect(vm.v.number.$silentErrors).toHaveLength(2)
expect(vm.v.number.$silentErrors).toContainEqual(externalErrorObject)
vm.v.number.$model = 2
await nextTick()
// assert that changing `$model` resets the $externalResults
expect(vm.v.number.$error).toBe(false)
expect(vm.v.number.$externalResults).toHaveLength(0)
// add back the externalError
vm.vuelidateExternalResults.number = 'foo'
// assert the error is back
expect(vm.v.number.$error).toBe(true)
expect(vm.v.number.$silentErrors).toHaveLength(1)
expect(vm.v.number.$silentErrors).toEqual([externalErrorObject])
expect(vm.v.number.$externalResults).toEqual([externalErrorObject])
vm.vuelidateExternalResults.number = []
expect(vm.v.number.$error).toBe(false)
expect(vm.v.number.$silentErrors).toEqual([])
})

it('works by replacing the entire external state, with pre-definition', async () => {
it('works by replacing the entire external state, with pre-definition, using `$autoDirty` to track changes', async () => {
const validation = {
number: { isEven }
}
const vuelidateExternalResults = { number: '' }
const { vm } = await createOldApiSimpleWrapper(validation, { number: 1, vuelidateExternalResults })
const { vm } = await createOldApiSimpleWrapper(validation, { number: 1, vuelidateExternalResults }, { $autoDirty: true })

vm.v.$touch()
expect(vm.vuelidateExternalResults).toEqual({ number: '' })
expect(vm.vuelidateExternalResults).toEqual(vuelidateExternalResults)

expect(vm.v).toHaveProperty('number', expect.any(Object))
expect(vm.v.number.$externalResults).toEqual([])
// set an external validation result
Object.assign(vm.vuelidateExternalResults, { number: ['foo'] })
// assert
const externalErrorObject = {
$message: 'foo',
$params: {},
$pending: false,
$property: 'number',
$propertyPath: 'number',
$response: null,
$uid: 'number-0',
$validator: '$externalResults'
}
expect(vm.v.number.$externalResults).toEqual([externalErrorObject])
expect(vm.v.number.$error).toBe(true)
expect(vm.v.number.$silentErrors).toHaveLength(2)
expect(vm.v.number.$silentErrors).toContainEqual(externalErrorObject)
vm.v.number.$model = 2
vm.number = 2
await nextTick()
// assert the externalResults was reset
expect(vm.v.number.$error).toBe(false)
expect(vm.v.number.$externalResults).toHaveLength(0)
// revert the external results error
Object.assign(vm.vuelidateExternalResults, { number: ['foo'] })
await nextTick()
// assert errors are visible again
expect(vm.v.number.$error).toBe(true)
expect(vm.v.number.$silentErrors).toHaveLength(1)
expect(vm.v.number.$silentErrors).toEqual([externalErrorObject])
Expand All @@ -394,14 +401,8 @@ describe('OptionsAPI validations', () => {
// trigger again
Object.assign(vm.vuelidateExternalResults, { number: ['bar'] })
expect(vm.v.number.$externalResults).toEqual([{
$message: 'bar',
$params: {},
$pending: false,
$property: 'number',
$propertyPath: 'number',
$response: null,
$uid: 'number-0',
$validator: '$externalResults'
...externalErrorObject,
$message: 'bar'
}])
})
})
Expand Down
Loading

0 comments on commit 67d56d7

Please sign in to comment.