Skip to content

Commit

Permalink
feat: add validation groups (vuelidate#1066)
Browse files Browse the repository at this point in the history
* docs: add explanation for validation groups removal

* feat: add back validation groups as a config

* docs: update docs about validation groups
  • Loading branch information
dobromir-hristov authored Jul 23, 2022
1 parent b9dd30c commit ad5091b
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 30 deletions.
36 changes: 36 additions & 0 deletions packages/docs/src/advanced_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -786,3 +786,39 @@ export default {
}
}
```
## Validation Groups
You may want to group a few validation rules under one roof, in which case a validation group is a perfect choice.
To create a validation group, you must specify a config property at the top level of your rules, called `$validationGroups`.
This is an object that holds the name of your groups and an array of property paths, which will be the group itself.
```js
const rules = {
number: { isEven },
nested: {
word: { required: v => !!v }
},
$validationGroups: {
firstGroup: ['number', 'nested.word']
}
}
```
In the above example, it will create a group called `firstGroup` that will reflect the state of `number` and `nested.word`.
You can see all your defined groups in the `v$.$validationGroups` property of your vue instance.
The group has the typical properties of other validations:
```ts
interface ValidationGroupItem {
$invalid: boolean,
$error: boolean,
$pending: boolean,
$errors: ErrorObject[],
$silentErrors: ErrorObject[]
}
```
26 changes: 26 additions & 0 deletions packages/docs/src/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,29 @@ export default {
}
}
```

## Validation groups change

Validation groups have been moved to the `$validationGroups` config.

### Migration strategy

To create a validation group, you must specify a config property at the top level of your rules, called `$validationGroups`

This is an object that holds validation groups, scoped under a name of your choice:

```js
const rules = {
number: { isEven },
nested: {
word: { required: v => !!v }
},
$validationGroups: {
firstName: ['number', 'nested.word']
}
}
```

In the above example, it will create a group called `firstName` that will reflect the state of `number` and `nested.word`.

You can see all your defined groups in the `v$.$validationGroups` property of your vue instance.
20 changes: 17 additions & 3 deletions packages/vuelidate/src/core.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { unwrap } from './utils'
import { unwrap, gatherBooleanGroupProperties, gatherArrayGroupProperties } from './utils'
import { computed, isRef, nextTick, reactive, ref, watch } from 'vue-demi'
import { createValidatorResult } from './utils/createResults'
import { sortValidations } from './utils/sortValidations'
Expand Down Expand Up @@ -399,7 +399,7 @@ export function setValidations ({
// – rules = validators for current state tree fragment
// — nestedValidators = nested state fragments keys that might contain more validators
// – config = configuration properties that affect this state fragment
const { rules, nestedValidators, config } = sortValidations(validations)
const { rules, nestedValidators, config, validationGroups } = sortValidations(validations)
const mergedConfig = { ...globalConfig, ...config }

// create protected state for cases when the state branch does not exist yet.
Expand All @@ -426,6 +426,19 @@ export function setValidations ({
// *WARN*: This is recursive
const nestedResults = collectNestedValidationResults(nestedValidators, nestedState, path, resultsCache, mergedConfig, instance, nestedExternalResults)

const $validationGroups = {}
if (validationGroups) {
Object.entries(validationGroups).forEach(([key, group]) => {
$validationGroups[key] = {
$invalid: gatherBooleanGroupProperties(group, nestedResults, '$invalid'),
$error: gatherBooleanGroupProperties(group, nestedResults, '$error'),
$pending: gatherBooleanGroupProperties(group, nestedResults, '$pending'),
$errors: gatherArrayGroupProperties(group, nestedResults, '$errors'),
$silentErrors: gatherArrayGroupProperties(group, nestedResults, '$silentErrors')
}
})
}

// Collect and merge this level validation results
// with all nested validation results
const {
Expand Down Expand Up @@ -543,7 +556,8 @@ export function setValidations ({
// if there are no child results, we are inside a nested property
...(childResults && {
$getResultsForChild,
$clearExternalResults
$clearExternalResults,
$validationGroups
}),
// add each nested property's state
...nestedResults
Expand Down
33 changes: 33 additions & 0 deletions packages/vuelidate/src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,36 @@ export function paramToRef (param) {
export function isProxy (value) {
return isReactive(value) || isReadonly(value)
}

export function get (obj, stringPath, def) {
// Cache the current object
let current = obj
const path = stringPath.split('.')
// For each item in the path, dig into the object
for (let i = 0; i < path.length; i++) {
// If the item isn't found, return the default (or null)
if (!current[path[i]]) return def

// Otherwise, update the current value
current = current[path[i]]
}

return current
}

export function gatherBooleanGroupProperties (group, nestedResults, property) {
return computed(() => {
return group.some((path) => {
return get(nestedResults, path, { [property]: false })[property]
})
})
}

export function gatherArrayGroupProperties (group, nestedResults, property) {
return computed(() => {
return group.reduce((all, path) => {
const fetchedProperty = get(nestedResults, path, { [property]: false })[property] || []
return all.concat(fetchedProperty)
}, [])
})
}
6 changes: 5 additions & 1 deletion packages/vuelidate/src/utils/sortValidations.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function sortValidations (validationsRaw = {}) {
const rules = {}
const nestedValidators = {}
const config = {}
let validationGroups = null

validationKeys.forEach(key => {
const v = validations[key]
Expand All @@ -26,6 +27,9 @@ export function sortValidations (validationsRaw = {}) {
case isFunction(v):
rules[key] = { $validator: v }
break
case key === '$validationGroups':
validationGroups = v
break
// Catch $-prefixed properties as config
case key.startsWith('$'):
config[key] = v
Expand All @@ -37,5 +41,5 @@ export function sortValidations (validationsRaw = {}) {
}
})

return { rules, nestedValidators, config }
return { rules, nestedValidators, config, validationGroups }
}
64 changes: 63 additions & 1 deletion packages/vuelidate/test/unit/specs/validation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
shouldBeInvalidValidationObject,
shouldBeErroredValidationObject,
createSimpleComponent,
shouldBeValidValidationObj, asyncTimeout
shouldBeValidValidationObj, asyncTimeout, buildErrorObject
} from '../utils'
import { withMessage, withParams } from '@vuelidate/validators/src/common'
import useVuelidate, { CollectFlag } from '../../../src'
Expand Down Expand Up @@ -2057,4 +2057,66 @@ describe('useVuelidate', () => {
})
})
})

describe('$validationGroups', () => {
it('should build validations from a group of items', async () => {
const number = ref(2)
const word = ref('abc')
const { vm } = await createSimpleWrapper({
number: { isEven },
nested: {
word: { required: v => !!v }
},
$validationGroups: {
firstName: ['number', 'nested.word']
}
}, { number, nested: { word } })

const numberError = buildErrorObject('number', 'number', 'isEven')
const wordError = buildErrorObject('word', 'nested.word', 'required')

expect(vm.v).toHaveProperty('number', expect.any(Object))
shouldBePristineValidationObj(vm.v.number)
shouldBePristineValidationObj(vm.v)
expect(vm.v).toHaveProperty('$validationGroups', {
firstName: expect.any(Object)
})
expect(vm.v.$validationGroups).toHaveProperty('firstName', {
$invalid: false,
$error: false,
$pending: false,
$errors: [],
$silentErrors: []
})
// make the word invalid
word.value = ''
// assert the validation group has re-calculated
expect(vm.v.$validationGroups.firstName).toEqual({
$invalid: true,
$error: false,
$pending: false,
$errors: [],
$silentErrors: [buildErrorObject('word', 'nested.word', 'required')]
})
// make the `number` dirty and invalid
vm.v.number.$model = 3
expect(vm.v.$validationGroups.firstName).toEqual({
$invalid: true,
$error: true,
$pending: false,
$errors: [numberError],
$silentErrors: [numberError, wordError]
})
// make both valid
vm.v.number.$model = 4
vm.v.nested.word.$model = 'foo'
expect(vm.v.$validationGroups.firstName).toEqual({
$invalid: false,
$error: false,
$pending: false,
$errors: [],
$silentErrors: []
})
})
})
})
36 changes: 11 additions & 25 deletions packages/vuelidate/test/unit/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,8 @@ export const shouldBeValidValidationObj = (v) => {
expect(v).toHaveProperty('$pending', false)
}

export const shouldBeInvalidValidationObject = ({ v, property, propertyPath = property, validatorName }) => {
expect(v).toHaveProperty('$error', false)
expect(v).toHaveProperty('$errors', [])
expect(v).toHaveProperty('$silentErrors', [{
export function buildErrorObject (property, propertyPath, validatorName) {
return {
$message: '',
$params: {},
$pending: false,
Expand All @@ -77,7 +75,13 @@ export const shouldBeInvalidValidationObject = ({ v, property, propertyPath = pr
$validator: validatorName,
$response: false,
$uid: `${propertyPath}-${validatorName}`
}])
}
}

export const shouldBeInvalidValidationObject = ({ v, property, propertyPath = property, validatorName }) => {
expect(v).toHaveProperty('$error', false)
expect(v).toHaveProperty('$errors', [])
expect(v).toHaveProperty('$silentErrors', [buildErrorObject(property, propertyPath, validatorName)])
expect(v).toHaveProperty('$invalid', true)
expect(v).toHaveProperty('$pending', false)
expect(v).toHaveProperty('$dirty', false)
Expand All @@ -88,26 +92,8 @@ export const shouldBeInvalidValidationObject = ({ v, property, propertyPath = pr

export const shouldBeErroredValidationObject = ({ v, property, propertyPath = property, validatorName }) => {
expect(v).toHaveProperty('$error', true)
expect(v).toHaveProperty('$errors', [{
$message: '',
$params: {},
$pending: false,
$property: property,
$propertyPath: propertyPath,
$validator: validatorName,
$response: false,
$uid: `${propertyPath}-${validatorName}`
}])
expect(v).toHaveProperty('$silentErrors', [{
$message: '',
$params: {},
$pending: false,
$property: property,
$propertyPath: propertyPath,
$validator: validatorName,
$response: false,
$uid: `${propertyPath}-${validatorName}`
}])
expect(v).toHaveProperty('$errors', [buildErrorObject(property, propertyPath, validatorName)])
expect(v).toHaveProperty('$silentErrors', [buildErrorObject(property, propertyPath, validatorName)])
expect(v).toHaveProperty('$invalid', true)
expect(v).toHaveProperty('$pending', false)
expect(v).toHaveProperty('$dirty', true)
Expand Down

0 comments on commit ad5091b

Please sign in to comment.