Skip to content

Commit

Permalink
feat(core): add external validations support via $externalResults (vu…
Browse files Browse the repository at this point in the history
…elidate#837), closes vuelidate#824

* feat: add external validators

* chore: add docs and $clearExternalResults method

* chore: fix issues with tests

* refactor: cleanup and add more tests

* chore: update docs

* chore: add type declarations for $externalResults
  • Loading branch information
dobromir-hristov authored Jun 19, 2021
1 parent 7527062 commit b259587
Show file tree
Hide file tree
Showing 9 changed files with 472 additions and 30 deletions.
93 changes: 92 additions & 1 deletion packages/docs/src/advanced_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ export default {
```

:::tip
You can pass validation configs as a single parameter to `useVuelidate` - [Passing a single parameter to useVuelidate](#passing-a-single-parameter-to-usevuelidate)
**Note:** You can pass validation configs as a single parameter to `useVuelidate`
- [Passing a single parameter to useVuelidate](#passing-a-single-parameter-to-usevuelidate)
:::

## Returning extra data from validators
Expand Down Expand Up @@ -273,6 +274,7 @@ in [Validation Configuration](./api/configuration.md).
If you prefer the Options API, you can specify a `validationConfig` object, that Vuelidate will read configs from.

```vue
<script>
import { useVuelidate } from '@vuelidate/core'
Expand Down Expand Up @@ -326,3 +328,92 @@ export default {
setup: () => ({ v$: useVuelidate({ $stopPropagation: true }) })
}
```

## Providing external validations, server side validation

To provide validation messages from an external source, like from a server side validation response, you can use the `$externalResults` functionality.
Each property in the validated state can have a corresponding string or array of strings as response message. This works with both Composition API and
Options API.

### External results with Composition API

When using the Composition API, you can pass a `reactive` or `ref` object, to the `$externalResults` global config.

```js
// inside setup
const state = reactive({ foo: '' });
const $externalResults = ref({}) // works with reactive({}) too.

const rules = { foo: { someValidation } }
const v = useVuelidate(rules, state, { $externalResults })

// validate method
async function validate () {
// check if everything is valid
if (!await v.value.$validate()) return
await doAsyncStuff()
// do server validation, and assume we have these errors
const errors = {
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)`
}

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.$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.

It is a good practice to pre-define your external results keys, to match your form structure, otherwise Vue may have a hard time tracking changes.

```js
export default {
data () {
return {
foo: '',
vuelidateExternalResults: {
foo: []
}
}
},
validations () {
return {
foo: { someValidation }
}
},
methods: {
validate () {
// perform validations
const errors = { foo: ['Error one', 'Error Two'] }
// merge the errors into the validation results
Object.assign(this.vuelidateExternalResults, errors)
}
}
}
```

To clear out the external results, you can again, use the `$clearExternalResults()` method

```js
async function validate() {
this.$v.$clearExternalResults()
// perform validations
const result = await this.runAsyncValidators()
}
```
6 changes: 6 additions & 0 deletions packages/docs/src/api/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@
* **Usage:**

Resets the `$dirty` state on all nested properties of a form.

## $clearExternalResults

* **Usage:**

Clears the `$externalResults` state back to an empty object.
9 changes: 8 additions & 1 deletion packages/vuelidate/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type BaseValidation <
readonly $error: boolean
readonly $errors: ErrorObject[]
readonly $silentErrors: ErrorObject[]
readonly $externalResults: ({ $validator: '$externalResults', $response: null, $pending: false, $params: {} } & ErrorObject)[]
readonly $invalid: boolean
readonly $anyDirty: boolean
readonly $pending: boolean
Expand All @@ -128,6 +129,7 @@ type NestedValidations <Vargs extends ValidationArgs = ValidationArgs, T = unkno
interface ChildValidations {
readonly $validate: () => Promise<boolean>
readonly $getResultsForChild: (key: string) => (BaseValidation & ChildValidations) | undefined
readonly $clearExternalResults: () => void
}

export type Validation <Vargs extends ValidationArgs = ValidationArgs, T = unknown> =
Expand Down Expand Up @@ -158,12 +160,17 @@ type ExtractState <Vargs extends ValidationArgs> = Vargs extends ValidationRuleC

type ToRefs <T> = { [K in keyof T]: Ref<T[K]> };

interface ServerErrors {
[key: string]: string | string[] | ServerErrors
}

interface GlobalConfig {
$registerAs?: string
$scope?: string | number | symbol | boolean
$stopPropagation?: boolean
$autoDirty?: boolean
$lazy?: boolean
$lazy?: boolean,
$externalResults: ServerErrors
}

export function useVuelidate(globalConfig?: GlobalConfig): Ref<Validation>;
Expand Down
84 changes: 67 additions & 17 deletions packages/vuelidate/src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ let ROOT_PATH = '__root'
/**
* Sorts the validators for a state tree branch
* @param {Object<NormalizedValidator|Function>} validationsRaw
* @return {{ rules: Object<NormalizedValidator>, nestedValidators: Object, config: Object }}
* @return {{ rules: Object<NormalizedValidator>, nestedValidators: Object, config: GlobalConfig }}
*/
function sortValidations (validationsRaw = {}) {
const validations = unwrap(validationsRaw)
Expand Down Expand Up @@ -104,7 +104,8 @@ function normalizeValidatorResponse (result) {
* @param {Ref<*>} model
* @param {Ref<Boolean>} $pending
* @param {Ref<Boolean>} $dirty
* @param {Object} config
* @param {GlobalConfig} config
* @param {boolean} config.$lazy
* @param {Ref<*>} $response
* @param {VueInstance} instance
* @param {Ref<*>[]} watchTargets
Expand Down Expand Up @@ -157,7 +158,7 @@ function createAsyncResult (rule, model, $pending, $dirty, { $lazy }, $response,
* @param {Validator} rule
* @param {Ref<*>} model
* @param {Ref<Boolean>} $dirty
* @param {Object} config
* @param {GlobalConfig} config
* @param {Boolean} config.$lazy
* @param {Ref<*>} $response
* @param {VueInstance} instance
Expand Down Expand Up @@ -185,7 +186,7 @@ function createSyncResult (rule, model, $dirty, { $lazy }, $response, instance)
* @param {NormalizedValidator} rule
* @param {Ref<*>} model
* @param {Ref<boolean>} $dirty
* @param {Object} config
* @param {GlobalConfig} config
* @param {VueInstance} instance
* @return {{ $params: *, $message: Ref<String>, $pending: Ref<Boolean>, $invalid: Ref<Boolean>, $response: Ref<*>, $unwatch: WatchStopHandle }}
*/
Expand Down Expand Up @@ -274,11 +275,12 @@ function createValidatorResult (rule, model, $dirty, config, instance) {
* @param {String} key - Key for the current state tree
* @param {ResultsStorage} [resultsCache] - A cache map of all the validators
* @param {String} [path] - the current property path
* @param {Object} [config] - the config object
* @param {GlobalConfig} [config] - the config object
* @param {VueInstance} instance
* @param {ComputedRef<Object>} externalResults
* @return {ValidationResult | {}}
*/
function createValidationResults (rules, model, key, resultsCache, path, config, instance) {
function createValidationResults (rules, model, key, resultsCache, path, config, instance, externalResults) {
// collect the property keys
const ruleKeys = Object.keys(rules)

Expand Down Expand Up @@ -323,7 +325,22 @@ function createValidationResults (rules, model, key, resultsCache, path, config,
)
})

result.$externalResults = computed(() => {
if (!externalResults.value) return []
return [].concat(externalResults.value).map((stringError, index) => ({
$propertyPath: path,
$property: key,
$validator: '$externalResults',
$uid: `${path}-${index}`,
$message: stringError,
$params: {},
$response: null,
$pending: false
}))
})

result.$invalid = computed(() =>
!!result.$externalResults.value.length ||
ruleKeys.some(ruleKey => unwrap(result[ruleKey].$invalid))
)

Expand All @@ -350,6 +367,7 @@ function createValidationResults (rules, model, key, resultsCache, path, config,
$pending: res.$pending
})
})
.concat(result.$externalResults.value)
)

result.$errors = computed(() => result.$dirty.value
Expand All @@ -372,10 +390,12 @@ function createValidationResults (rules, model, key, resultsCache, path, config,
* @param {Object} nestedState - Current state
* @param {String} path - Path to current property
* @param {ResultsStorage} resultsCache - Validations cache map
* @param {Object} config - The config object
* @param {GlobalConfig} config - The config object
* @param {VueInstance} instance - The current Vue instance
* @param {ComputedRef<object>} nestedExternalResults - The external results for this nested collection
* @return {{}}
*/
function collectNestedValidationResults (validations, nestedState, path, resultsCache, config, instance) {
function collectNestedValidationResults (validations, nestedState, path, resultsCache, config, instance, nestedExternalResults) {
const nestedValidationKeys = Object.keys(validations)

// if we have no state, return empty object
Expand All @@ -390,7 +410,8 @@ function collectNestedValidationResults (validations, nestedState, path, results
parentKey: path,
resultsCache,
globalConfig: config,
instance
instance,
externalResults: nestedExternalResults
})
return results
}, {})
Expand All @@ -399,8 +420,8 @@ function collectNestedValidationResults (validations, nestedState, path, results
/**
* Generates the Meta fields from the results
* @param {ValidationResult|{}} results
* @param {Object<ValidationResult>[]} nestedResults
* @param {Object<ValidationResult>[]} childResults
* @param {Object.<string, ValidationResult>[]} nestedResults
* @param {Object.<string, ValidationResult>[]} childResults
* @return {{$anyDirty: Ref<Boolean>, $error: Ref<Boolean>, $invalid: Ref<Boolean>, $errors: Ref<ErrorObject[]>, $dirty: Ref<Boolean>, $touch: Function, $reset: Function }}
*/
function createMetaFields (results, nestedResults, childResults) {
Expand Down Expand Up @@ -540,8 +561,10 @@ function createMetaFields (results, nestedResults, childResults) {
* @param {String} [params.key] - Current state property key. Used when being called on nested items
* @param {String} [params.parentKey] - Parent state property key. Used when being called recursively
* @param {Object<ValidationResult>} [params.childResults] - Used to collect child results.
* @param {ResultsStorage} resultsCache - The cached validation results
* @param {VueInstance} instance - The current Vue instance
* @param {ResultsStorage} params.resultsCache - The cached validation results
* @param {VueInstance} params.instance - The current Vue instance
* @param {GlobalConfig} params.globalConfig - The validation config, passed to this setValidations instance.
* @param {Reactive<object> | Ref<Object>} params.externalResults - External validation results
* @return {UnwrapNestedRefs<VuelidateState>}
*/
export function setValidations ({
Expand All @@ -552,7 +575,8 @@ export function setValidations ({
childResults,
resultsCache,
globalConfig = {},
instance
instance,
externalResults
}) {
const path = parentKey ? `${parentKey}.${key}` : key

Expand All @@ -572,11 +596,20 @@ export function setValidations ({
})
: state

// cache the external results, so we can revert back to them
const cachedExternalResults = { ...(unwrap(externalResults) || {}) }

const nestedExternalResults = computed(() => {
const results = unwrap(externalResults)
if (!key) return results
return results ? unwrap(results[key]) : undefined
})

// Use rules for the current state fragment and validate it
const results = createValidationResults(rules, nestedState, key, resultsCache, path, mergedConfig, instance)
const results = createValidationResults(rules, nestedState, key, resultsCache, path, mergedConfig, instance, nestedExternalResults)
// Use nested keys to repeat the process
// *WARN*: This is recursive
const nestedResults = collectNestedValidationResults(nestedValidators, nestedState, path, resultsCache, mergedConfig, instance)
const nestedResults = collectNestedValidationResults(nestedValidators, nestedState, path, resultsCache, mergedConfig, instance, nestedExternalResults)

// Collect and merge this level validation results
// with all nested validation results
Expand Down Expand Up @@ -646,6 +679,22 @@ export function setValidations ({
return (childResults.value || {})[key]
}

function $clearExternalResults () {
if (isRef(externalResults)) {
externalResults.value = cachedExternalResults
} else {
// if the external results state was empty, we need to delete every property, one by one
if (Object.keys(cachedExternalResults).length === 0) {
Object.keys(externalResults).forEach((k) => {
delete externalResults[k]
})
} else {
// state was not empty, so we just assign it back into the current state
Object.assign(externalResults, cachedExternalResults)
}
}
}

return reactive({
...results,
// NOTE: The order here is very important, since we want to override
Expand All @@ -665,7 +714,8 @@ export function setValidations ({
// if there are no child results, we are inside a nested property
...(childResults && {
$getResultsForChild,
$validate
$validate,
$clearExternalResults
}),
// add each nested property's state
...nestedResults
Expand Down
9 changes: 6 additions & 3 deletions packages/vuelidate/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function nestedValidations ({ $scope }) {
* @property {String} [$registerAs] - Config Object
* @property {String | Number | Symbol} [$scope] - A scope to limit child component registration
* @property {Boolean} [$stopPropagation] - Tells a Vue component to stop sending it's results up to the parent
* @property {Ref<Object>} [$externalResults] - External error messages, like from server validation.
*/

/**
Expand All @@ -92,7 +93,7 @@ export function useVuelidate (validations, state, globalConfig = {}) {
validations = undefined
state = undefined
}
let { $registerAs, $scope = CollectFlag.COLLECT_ALL, $stopPropagation } = globalConfig
let { $registerAs, $scope = CollectFlag.COLLECT_ALL, $stopPropagation, $externalResults } = globalConfig

let instance = getCurrentInstance()

Expand Down Expand Up @@ -148,7 +149,8 @@ export function useVuelidate (validations, state, globalConfig = {}) {
childResults,
resultsCache,
globalConfig,
instance: instance.proxy
instance: instance.proxy,
externalResults: instance.proxy.vuelidateExternalResults
})
}, { immediate: true })
})
Expand All @@ -167,7 +169,8 @@ export function useVuelidate (validations, state, globalConfig = {}) {
childResults,
resultsCache,
globalConfig,
instance: instance ? instance.proxy : {}
instance: instance ? instance.proxy : {},
externalResults: $externalResults
})
}, {
immediate: true
Expand Down
Loading

0 comments on commit b259587

Please sign in to comment.