Merge pull request #10 from cmath10/proposal-refactor#validation
Validation refactor
This commit is contained in:
commit
01357aef1e
@ -1,22 +1,22 @@
|
|||||||
import { VueConstructor } from 'vue'
|
import { VueConstructor } from 'vue'
|
||||||
|
|
||||||
import { has } from '@/libs/utils'
|
|
||||||
import rules from '@/validation/rules'
|
|
||||||
import messages from '@/validation/messages'
|
|
||||||
import merge from '@/utils/merge'
|
import merge from '@/utils/merge'
|
||||||
|
import validationRules from '@/validation/rules'
|
||||||
|
import validationMessages from '@/validation/messages'
|
||||||
|
|
||||||
import FormularioForm from '@/FormularioForm.vue'
|
import FormularioForm from '@/FormularioForm.vue'
|
||||||
import FormularioInput from '@/FormularioInput.vue'
|
|
||||||
import FormularioGrouping from '@/FormularioGrouping.vue'
|
import FormularioGrouping from '@/FormularioGrouping.vue'
|
||||||
|
import FormularioInput from '@/FormularioInput.vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ValidationContext,
|
ValidationContext,
|
||||||
ValidationRule,
|
CheckRuleFn,
|
||||||
} from '@/validation/types'
|
CreateMessageFn,
|
||||||
|
} from '@/validation/validator'
|
||||||
|
|
||||||
interface FormularioOptions {
|
interface FormularioOptions {
|
||||||
rules?: any;
|
validationRules?: any;
|
||||||
validationMessages?: any;
|
validationMessages?: Record<string, Function>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
@ -24,15 +24,12 @@ interface FormularioOptions {
|
|||||||
* The base formulario library.
|
* The base formulario library.
|
||||||
*/
|
*/
|
||||||
export default class Formulario {
|
export default class Formulario {
|
||||||
public options: FormularioOptions
|
public validationRules: Record<string, CheckRuleFn> = {}
|
||||||
public idRegistry: { [name: string]: number }
|
public validationMessages: Record<string, Function> = {}
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.options = {
|
this.validationRules = validationRules
|
||||||
rules,
|
this.validationMessages = validationMessages
|
||||||
validationMessages: messages,
|
|
||||||
}
|
|
||||||
this.idRegistry = {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,47 +44,38 @@ export default class Formulario {
|
|||||||
this.extend(options || {})
|
this.extend(options || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Produce a deterministically generated id based on the sequence by which it
|
|
||||||
* was requested. This should be *theoretically* the same SSR as client side.
|
|
||||||
* However, SSR and deterministic ids can be very challenging, so this
|
|
||||||
* implementation is open to community review.
|
|
||||||
*/
|
|
||||||
nextId (vm: Vue): string {
|
|
||||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
|
||||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
|
||||||
if (!has(this.idRegistry, pathPrefix)) {
|
|
||||||
this.idRegistry[pathPrefix] = 0
|
|
||||||
}
|
|
||||||
return `formulario-${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a set of options, apply them to the pre-existing options.
|
* Given a set of options, apply them to the pre-existing options.
|
||||||
*/
|
*/
|
||||||
extend (extendWith: FormularioOptions): Formulario {
|
extend (extendWith: FormularioOptions): Formulario {
|
||||||
if (typeof extendWith === 'object') {
|
if (typeof extendWith === 'object') {
|
||||||
this.options = merge(this.options, extendWith)
|
this.validationRules = merge(this.validationRules, extendWith.validationRules || {})
|
||||||
|
this.validationMessages = merge(this.validationMessages, extendWith.validationMessages || {})
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
throw new Error(`[Formulario]: Formulario.extend() should be passed an object (was ${typeof extendWith})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get validation rules by merging any passed in with global rules.
|
* Get validation rules by merging any passed in with global rules.
|
||||||
*/
|
*/
|
||||||
rules (rules: Record<string, ValidationRule> = {}): () => Record<string, ValidationRule> {
|
getRules (extendWith: Record<string, CheckRuleFn> = {}): Record<string, CheckRuleFn> {
|
||||||
return { ...this.options.rules, ...rules }
|
return merge(this.validationRules, extendWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation message for a particular error.
|
* Get validation messages by merging any passed in with global messages.
|
||||||
*/
|
*/
|
||||||
validationMessage (rule: string, context: ValidationContext, vm: Vue): string {
|
getMessages (vm: Vue, extendWith: Record<string, Function>): Record<string, CreateMessageFn> {
|
||||||
if (has(this.options.validationMessages, rule)) {
|
const raw = merge(this.validationMessages || {}, extendWith)
|
||||||
return this.options.validationMessages[rule](vm, context)
|
const messages: Record<string, CreateMessageFn> = {}
|
||||||
} else {
|
|
||||||
return this.options.validationMessages.default(vm, context)
|
for (const name in raw) {
|
||||||
|
messages[name] = (context: ValidationContext, ...args: any[]): string => {
|
||||||
|
return typeof raw[name] === 'string' ? raw[name] : raw[name](vm, context, ...args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import {
|
import { Component, Model, Prop, Provide, Watch } from 'vue-property-decorator'
|
||||||
Component,
|
import { clone, getNested, has, merge, setNested, shallowEqualObjects } from '@/utils'
|
||||||
Model,
|
|
||||||
Prop,
|
|
||||||
Provide,
|
|
||||||
Watch,
|
|
||||||
} from 'vue-property-decorator'
|
|
||||||
import { cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
|
||||||
import merge from '@/utils/merge'
|
|
||||||
import Registry from '@/form/registry'
|
import Registry from '@/form/registry'
|
||||||
import FormularioInput from '@/FormularioInput.vue'
|
import FormularioInput from '@/FormularioInput.vue'
|
||||||
|
|
||||||
@ -24,7 +17,7 @@ import {
|
|||||||
ErrorObserverRegistry,
|
ErrorObserverRegistry,
|
||||||
} from '@/validation/ErrorObserver'
|
} from '@/validation/ErrorObserver'
|
||||||
|
|
||||||
import { ValidationErrorBag } from '@/validation/types'
|
import { Violation } from '@/validation/validator'
|
||||||
|
|
||||||
@Component({ name: 'FormularioForm' })
|
@Component({ name: 'FormularioForm' })
|
||||||
export default class FormularioForm extends Vue {
|
export default class FormularioForm extends Vue {
|
||||||
@ -41,13 +34,22 @@ export default class FormularioForm extends Vue {
|
|||||||
|
|
||||||
public proxy: Record<string, any> = {}
|
public proxy: Record<string, any> = {}
|
||||||
|
|
||||||
registry: Registry = new Registry(this)
|
private registry: Registry = new Registry(this)
|
||||||
|
|
||||||
private errorObserverRegistry = new ErrorObserverRegistry()
|
private errorObserverRegistry = new ErrorObserverRegistry()
|
||||||
// Local error messages are temporal, they wiped each resetValidation call
|
// Local error messages are temporal, they wiped each resetValidation call
|
||||||
private localFormErrors: string[] = []
|
private localFormErrors: string[] = []
|
||||||
private localFieldErrors: Record<string, string[]> = {}
|
private localFieldErrors: Record<string, string[]> = {}
|
||||||
|
|
||||||
|
get initialValues (): Record<string, any> {
|
||||||
|
if (this.hasModel && typeof this.formularioValue === 'object') {
|
||||||
|
// If there is a v-model on the form/group, use those values as first priority
|
||||||
|
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
get mergedFormErrors (): string[] {
|
get mergedFormErrors (): string[] {
|
||||||
return [...this.formErrors, ...this.localFormErrors]
|
return [...this.formErrors, ...this.localFormErrors]
|
||||||
}
|
}
|
||||||
@ -64,15 +66,6 @@ export default class FormularioForm extends Vue {
|
|||||||
return this.formularioValue && typeof this.formularioValue === 'object'
|
return this.formularioValue && typeof this.formularioValue === 'object'
|
||||||
}
|
}
|
||||||
|
|
||||||
get initialValues (): Record<string, any> {
|
|
||||||
if (this.hasModel && typeof this.formularioValue === 'object') {
|
|
||||||
// If there is a v-model on the form/group, use those values as first priority
|
|
||||||
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('formularioValue', { deep: true })
|
@Watch('formularioValue', { deep: true })
|
||||||
onFormularioValueChanged (values: Record<string, any>): void {
|
onFormularioValueChanged (values: Record<string, any>): void {
|
||||||
if (this.hasModel && values && typeof values === 'object') {
|
if (this.hasModel && values && typeof values === 'object') {
|
||||||
@ -101,7 +94,7 @@ export default class FormularioForm extends Vue {
|
|||||||
|
|
||||||
onFormSubmit (): Promise<void> {
|
onFormSubmit (): Promise<void> {
|
||||||
return this.hasValidationErrors()
|
return this.hasValidationErrors()
|
||||||
.then(hasErrors => hasErrors ? undefined : cloneDeep(this.proxy))
|
.then(hasErrors => hasErrors ? undefined : clone(this.proxy))
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (typeof data !== 'undefined') {
|
if (typeof data !== 'undefined') {
|
||||||
this.$emit('submit', data)
|
this.$emit('submit', data)
|
||||||
@ -112,8 +105,8 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
onFormularioFieldValidation (errorBag: ValidationErrorBag): void {
|
onFormularioFieldValidation (payload: { name: string; violations: Violation[]}): void {
|
||||||
this.$emit('validation', errorBag)
|
this.$emit('validation', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
@ -193,19 +186,18 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
|
||||||
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
|
|
||||||
resolvers.push(input.performValidation() && input.hasValidationErrors())
|
|
||||||
return resolvers
|
|
||||||
}, [])).then(results => results.some(hasErrors => hasErrors))
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
||||||
// given an object of errors, apply them to this form
|
|
||||||
this.localFormErrors = formErrors || []
|
this.localFormErrors = formErrors || []
|
||||||
this.localFieldErrors = inputErrors || {}
|
this.localFieldErrors = inputErrors || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasValidationErrors (): Promise<boolean> {
|
||||||
|
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
|
||||||
|
resolvers.push(input.runValidation() && input.hasValidationErrors())
|
||||||
|
return resolvers
|
||||||
|
}, [])).then(results => results.some(hasErrors => hasErrors))
|
||||||
|
}
|
||||||
|
|
||||||
resetValidation (): void {
|
resetValidation (): void {
|
||||||
this.localFormErrors = []
|
this.localFormErrors = []
|
||||||
this.localFieldErrors = {}
|
this.localFieldErrors = {}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="formulario-input">
|
<div class="formulario-input">
|
||||||
<slot
|
<slot :context="context" />
|
||||||
:id="id"
|
|
||||||
:context="context"
|
|
||||||
:violations="validationErrors"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -17,21 +13,17 @@ import {
|
|||||||
Prop,
|
Prop,
|
||||||
Watch,
|
Watch,
|
||||||
} from 'vue-property-decorator'
|
} from 'vue-property-decorator'
|
||||||
import { arrayify, has, parseRules, shallowEqualObjects, snakeToCamel } from './libs/utils'
|
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './utils'
|
||||||
import {
|
import {
|
||||||
ValidationContext,
|
CheckRuleFn,
|
||||||
ValidationError,
|
CreateMessageFn,
|
||||||
ValidationRule,
|
processConstraints,
|
||||||
} from '@/validation/types'
|
|
||||||
import {
|
|
||||||
createValidatorGroups,
|
|
||||||
validate,
|
validate,
|
||||||
Validator,
|
Violation,
|
||||||
ValidatorGroup,
|
|
||||||
} from '@/validation/validator'
|
} from '@/validation/validator'
|
||||||
|
|
||||||
const ERROR_BEHAVIOR = {
|
const VALIDATION_BEHAVIOR = {
|
||||||
BLUR: 'blur',
|
DEMAND: 'demand',
|
||||||
LIVE: 'live',
|
LIVE: 'live',
|
||||||
SUBMIT: 'submit',
|
SUBMIT: 'submit',
|
||||||
}
|
}
|
||||||
@ -47,53 +39,78 @@ export default class FormularioInput extends Vue {
|
|||||||
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined
|
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined
|
||||||
@Inject({ default: '' }) path!: string
|
@Inject({ default: '' }) path!: string
|
||||||
|
|
||||||
@Model('input', { default: '' }) formularioValue: any
|
@Model('input', { default: '' }) value!: any
|
||||||
|
|
||||||
@Prop({ default: null }) id!: string|number|null
|
|
||||||
@Prop({ required: true }) name!: string
|
|
||||||
@Prop({ default: false }) value!: any
|
|
||||||
@Prop({ default: '' }) validation!: string|any[]
|
|
||||||
@Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRule>
|
|
||||||
@Prop({ default: () => ({}) }) validationMessages!: Record<string, any>
|
|
||||||
@Prop({ default: () => [] }) errors!: string[]
|
|
||||||
@Prop({
|
@Prop({
|
||||||
default: ERROR_BEHAVIOR.BLUR,
|
required: true,
|
||||||
validator: behavior => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(behavior)
|
validator: (name: any): boolean => typeof name === 'string' && name.length > 0,
|
||||||
}) errorBehavior!: string
|
}) name!: string
|
||||||
|
|
||||||
@Prop({ default: false }) disableErrors!: boolean
|
@Prop({ default: '' }) validation!: string|any[]
|
||||||
|
@Prop({ default: () => ({}) }) validationRules!: Record<string, CheckRuleFn>
|
||||||
|
@Prop({ default: () => ({}) }) validationMessages!: Record<string, CreateMessageFn|string>
|
||||||
|
@Prop({
|
||||||
|
default: VALIDATION_BEHAVIOR.DEMAND,
|
||||||
|
validator: behavior => Object.values(VALIDATION_BEHAVIOR).includes(behavior)
|
||||||
|
}) validationBehavior!: string
|
||||||
|
|
||||||
defaultId: string = this.$formulario.nextId(this)
|
// Affects only observing & setting of local errors
|
||||||
proxy: Record<string, any> = this.getInitialValue()
|
@Prop({ default: false }) errorsDisabled!: boolean
|
||||||
localErrors: string[] = []
|
|
||||||
validationErrors: ValidationError[] = []
|
public proxy: any = this.getInitialValue()
|
||||||
pendingValidation: Promise<any> = Promise.resolve()
|
|
||||||
|
private localErrors: string[] = []
|
||||||
|
private violations: Violation[] = []
|
||||||
|
private validationRun: Promise<any> = Promise.resolve()
|
||||||
|
|
||||||
|
get fullQualifiedName (): string {
|
||||||
|
return this.path !== '' ? `${this.path}.${this.name}` : this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
get model (): any {
|
||||||
|
const model = this.hasModel ? 'value' : 'proxy'
|
||||||
|
if (this[model] === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return this[model]
|
||||||
|
}
|
||||||
|
|
||||||
|
set model (value: any) {
|
||||||
|
if (!shallowEqualObjects(value, this.proxy)) {
|
||||||
|
this.proxy = value
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('input', value)
|
||||||
|
|
||||||
|
if (typeof this.formularioSetter === 'function') {
|
||||||
|
this.formularioSetter(this.context.name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get context (): Record<string, any> {
|
get context (): Record<string, any> {
|
||||||
return Object.defineProperty({
|
return Object.defineProperty({
|
||||||
id: this.id || this.defaultId,
|
name: this.fullQualifiedName,
|
||||||
name: this.nameOrFallback,
|
runValidation: this.runValidation.bind(this),
|
||||||
blurHandler: this.blurHandler.bind(this),
|
violations: this.violations,
|
||||||
errors: this.explicitErrors,
|
errors: this.localErrors,
|
||||||
allErrors: this.allErrors,
|
allErrors: [...this.localErrors, ...this.violations.map(v => v.message)],
|
||||||
performValidation: this.performValidation.bind(this),
|
|
||||||
validationErrors: this.validationErrors,
|
|
||||||
value: this.value,
|
|
||||||
}, 'model', {
|
}, 'model', {
|
||||||
get: this.modelGetter.bind(this),
|
get: () => this.model,
|
||||||
set: this.modelSetter.bind(this),
|
set: (value: any) => {
|
||||||
|
this.model = value
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get parsedValidationRules (): Record<string, ValidationRule> {
|
get normalizedValidationRules (): Record<string, CheckRuleFn> {
|
||||||
const parsedValidationRules: Record<string, ValidationRule> = {}
|
const rules: Record<string, CheckRuleFn> = {}
|
||||||
Object.keys(this.validationRules).forEach(key => {
|
Object.keys(this.validationRules).forEach(key => {
|
||||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
rules[snakeToCamel(key)] = this.validationRules[key]
|
||||||
})
|
})
|
||||||
return parsedValidationRules
|
return rules
|
||||||
}
|
}
|
||||||
|
|
||||||
get messages (): Record<string, any> {
|
get normalizedValidationMessages (): Record<string, any> {
|
||||||
const messages: Record<string, any> = {}
|
const messages: Record<string, any> = {}
|
||||||
Object.keys(this.validationMessages).forEach((key) => {
|
Object.keys(this.validationMessages).forEach((key) => {
|
||||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||||
@ -101,131 +118,60 @@ export default class FormularioInput extends Vue {
|
|||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the element’s name, or select a fallback.
|
|
||||||
*/
|
|
||||||
get nameOrFallback (): string {
|
|
||||||
return this.path !== '' ? `${this.path}.${this.name}` : this.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does this computed property have errors
|
|
||||||
*/
|
|
||||||
get hasErrors (): boolean {
|
|
||||||
return this.allErrors.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The merged errors computed property.
|
|
||||||
* Each error is an object with fields message (translated message), rule (rule name) and context
|
|
||||||
*/
|
|
||||||
get allErrors (): ValidationError[] {
|
|
||||||
return [
|
|
||||||
...this.explicitErrors.map(message => ({ message })),
|
|
||||||
...arrayify(this.validationErrors)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* These are errors we that have been explicitly passed to us.
|
|
||||||
*/
|
|
||||||
get explicitErrors (): string[] {
|
|
||||||
return [...arrayify(this.errors), ...this.localErrors]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if this formulario element is v-modeled or not.
|
* Determines if this formulario element is v-modeled or not.
|
||||||
*/
|
*/
|
||||||
get hasModel (): boolean {
|
get hasModel (): boolean {
|
||||||
return has(this.$options.propsData || {}, 'formularioValue')
|
return has(this.$options.propsData || {}, 'value')
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('proxy')
|
@Watch('proxy')
|
||||||
onProxyChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void {
|
onProxyChanged (newValue: any, oldValue: any): void {
|
||||||
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
|
||||||
this.performValidation()
|
|
||||||
} else {
|
|
||||||
this.validationErrors = []
|
|
||||||
}
|
|
||||||
if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
this.context.model = newValue
|
this.context.model = newValue
|
||||||
}
|
}
|
||||||
|
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
|
||||||
|
this.runValidation()
|
||||||
|
} else {
|
||||||
|
this.violations = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('formularioValue')
|
@Watch('value')
|
||||||
onFormularioValueChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void {
|
onValueChanged (newValue: any, oldValue: any): void {
|
||||||
if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
this.context.model = newValue
|
this.context.model = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
created (): void {
|
created (): void {
|
||||||
this.applyInitialValue()
|
this.initProxy()
|
||||||
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
if (typeof this.formularioRegister === 'function') {
|
||||||
this.formularioRegister(this.nameOrFallback, this)
|
this.formularioRegister(this.fullQualifiedName, this)
|
||||||
}
|
}
|
||||||
if (!this.disableErrors && typeof this.addErrorObserver === 'function') {
|
if (typeof this.addErrorObserver === 'function' && !this.errorsDisabled) {
|
||||||
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.fullQualifiedName })
|
||||||
}
|
}
|
||||||
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
|
||||||
this.performValidation()
|
this.runValidation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
beforeDestroy (): void {
|
beforeDestroy (): void {
|
||||||
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
if (!this.errorsDisabled && typeof this.removeErrorObserver === 'function') {
|
||||||
this.removeErrorObserver(this.setErrors)
|
this.removeErrorObserver(this.setErrors)
|
||||||
}
|
}
|
||||||
if (typeof this.formularioDeregister === 'function') {
|
if (typeof this.formularioDeregister === 'function') {
|
||||||
this.formularioDeregister(this.nameOrFallback)
|
this.formularioDeregister(this.fullQualifiedName)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value from a model.
|
|
||||||
*/
|
|
||||||
modelGetter (): any {
|
|
||||||
const model = this.hasModel ? 'formularioValue' : 'proxy'
|
|
||||||
if (this[model] === undefined) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return this[model]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value from a model.
|
|
||||||
*/
|
|
||||||
modelSetter (value: any): void {
|
|
||||||
if (!shallowEqualObjects(value, this.proxy)) {
|
|
||||||
this.proxy = value
|
|
||||||
}
|
|
||||||
this.$emit('input', value)
|
|
||||||
if (this.context.name && typeof this.formularioSetter === 'function') {
|
|
||||||
this.formularioSetter(this.context.name, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bound into the context object.
|
|
||||||
*/
|
|
||||||
blurHandler (): void {
|
|
||||||
this.$emit('blur')
|
|
||||||
if (this.errorBehavior === ERROR_BEHAVIOR.BLUR) {
|
|
||||||
this.performValidation()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitialValue (): any {
|
getInitialValue (): any {
|
||||||
if (has(this.$options.propsData as Record<string, any>, 'value')) {
|
return has(this.$options.propsData || {}, 'value') ? this.value : ''
|
||||||
return this.value
|
|
||||||
} else if (has(this.$options.propsData as Record<string, any>, 'formularioValue')) {
|
|
||||||
return this.formularioValue
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyInitialValue (): void {
|
initProxy (): void {
|
||||||
// This should only be run immediately on created and ensures that the
|
// This should only be run immediately on created and ensures that the
|
||||||
// proxy and the model are both the same before any additional registration.
|
// proxy and the model are both the same before any additional registration.
|
||||||
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
||||||
@ -233,98 +179,42 @@ export default class FormularioInput extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
performValidation (): Promise<void> {
|
runValidation (): Promise<void> {
|
||||||
this.pendingValidation = this.validate().then(errors => {
|
this.validationRun = this.validate().then(violations => {
|
||||||
this.didValidate(errors)
|
const validationChanged = !shallowEqualObjects(violations, this.violations)
|
||||||
})
|
this.violations = violations
|
||||||
return this.pendingValidation
|
if (validationChanged) {
|
||||||
}
|
const payload = {
|
||||||
|
name: this.context.name,
|
||||||
applyValidator (validator: Validator): Promise<ValidationError|false> {
|
violations: this.violations,
|
||||||
return validate(validator, {
|
}
|
||||||
value: this.context.model,
|
this.$emit('validation', payload)
|
||||||
name: this.context.name,
|
if (typeof this.onFormularioFieldValidation === 'function') {
|
||||||
getFormValues: this.getFormValues.bind(this),
|
this.onFormularioFieldValidation(payload)
|
||||||
}).then(valid => valid ? false : this.getMessageObject(validator.name, validator.args))
|
|
||||||
}
|
|
||||||
|
|
||||||
applyValidatorGroup (group: ValidatorGroup): Promise<ValidationError[]> {
|
|
||||||
return Promise.all(group.validators.map(this.applyValidator))
|
|
||||||
.then(violations => (violations.filter(v => v !== false) as ValidationError[]))
|
|
||||||
}
|
|
||||||
|
|
||||||
validate (): Promise<ValidationError[]> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const resolveGroups = (groups: ValidatorGroup[], all: ValidationError[] = []): void => {
|
|
||||||
if (groups.length) {
|
|
||||||
const current = groups.shift() as ValidatorGroup
|
|
||||||
|
|
||||||
this.applyValidatorGroup(current).then(violations => {
|
|
||||||
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
|
||||||
if ((violations.length === 0 || !current.bail) && groups.length) {
|
|
||||||
return resolveGroups(groups, all.concat(violations))
|
|
||||||
}
|
|
||||||
return resolve(all.concat(violations))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolve([])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolveGroups(createValidatorGroups(
|
|
||||||
parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
return this.violations
|
||||||
))
|
|
||||||
})
|
})
|
||||||
|
return this.validationRun
|
||||||
}
|
}
|
||||||
|
|
||||||
didValidate (violations: ValidationError[]): void {
|
validate (): Promise<Violation[]> {
|
||||||
const validationChanged = !shallowEqualObjects(violations, this.validationErrors)
|
return validate(processConstraints(
|
||||||
this.validationErrors = violations
|
this.validation,
|
||||||
if (validationChanged) {
|
this.$formulario.getRules(this.normalizedValidationRules),
|
||||||
const errorBag = {
|
this.$formulario.getMessages(this, this.normalizedValidationMessages),
|
||||||
name: this.context.name,
|
), {
|
||||||
errors: this.validationErrors,
|
|
||||||
}
|
|
||||||
this.$emit('validation', errorBag)
|
|
||||||
if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') {
|
|
||||||
this.onFormularioFieldValidation(errorBag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMessageObject (ruleName: string | undefined, args: any[]): ValidationError {
|
|
||||||
const context = {
|
|
||||||
args,
|
|
||||||
name: this.name,
|
|
||||||
value: this.context.model,
|
value: this.context.model,
|
||||||
|
name: this.context.name,
|
||||||
formValues: this.getFormValues(),
|
formValues: this.getFormValues(),
|
||||||
}
|
})
|
||||||
const message = this.getMessageFunc(ruleName || '')(context)
|
|
||||||
|
|
||||||
return {
|
|
||||||
rule: ruleName,
|
|
||||||
context,
|
|
||||||
message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMessageFunc (ruleName: string): Function {
|
|
||||||
ruleName = snakeToCamel(ruleName)
|
|
||||||
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
|
||||||
switch (typeof this.messages[ruleName]) {
|
|
||||||
case 'function':
|
|
||||||
return this.messages[ruleName]
|
|
||||||
case 'string':
|
|
||||||
case 'boolean':
|
|
||||||
return (): string => this.messages[ruleName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (context: ValidationContext): string => this.$formulario.validationMessage(ruleName, context, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
hasValidationErrors (): Promise<boolean> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
this.validationRun.then(() => resolve(this.violations.length > 0))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -335,7 +225,7 @@ export default class FormularioInput extends Vue {
|
|||||||
|
|
||||||
resetValidation (): void {
|
resetValidation (): void {
|
||||||
this.localErrors = []
|
this.localErrors = []
|
||||||
this.validationErrors = []
|
this.violations = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { shallowEqualObjects, has, getNested } from '@/libs/utils'
|
import { shallowEqualObjects, has, getNested } from '@/utils'
|
||||||
import FormularioForm from '@/FormularioForm.vue'
|
import FormularioForm from '@/FormularioForm.vue'
|
||||||
import FormularioInput from '@/FormularioInput.vue'
|
import FormularioInput from '@/FormularioInput.vue'
|
||||||
|
|
||||||
@ -80,15 +80,6 @@ export default class Registry {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Map over the registry (recursively).
|
|
||||||
*/
|
|
||||||
map (mapper: Function): Record<string, any> {
|
|
||||||
const value = {}
|
|
||||||
this.registry.forEach((component, field) => Object.assign(value, { [field]: mapper(component, field) }))
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map over the registry (recursively).
|
* Map over the registry (recursively).
|
||||||
*/
|
*/
|
||||||
@ -115,8 +106,7 @@ export default class Registry {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.registry.set(field, component)
|
this.registry.set(field, component)
|
||||||
const hasModel = has(component.$options.propsData || {}, 'formularioValue')
|
const hasModel = has(component.$options.propsData || {}, 'value')
|
||||||
const hasValue = has(component.$options.propsData || {}, 'value')
|
|
||||||
if (
|
if (
|
||||||
!hasModel &&
|
!hasModel &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -129,7 +119,7 @@ export default class Registry {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
component.context.model = getNested(this.ctx.initialValues, field)
|
component.context.model = getNested(this.ctx.initialValues, field)
|
||||||
} else if (
|
} else if (
|
||||||
(hasModel || hasValue) &&
|
hasModel &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
||||||
) {
|
) {
|
||||||
@ -138,11 +128,6 @@ export default class Registry {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.ctx.setFieldValue(field, component.proxy)
|
this.ctx.setFieldValue(field, component.proxy)
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
if (this.ctx.childrenShouldShowErrors) {
|
|
||||||
// @ts-ignore
|
|
||||||
component.formShouldShowErrors = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,279 +0,0 @@
|
|||||||
export function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
|
||||||
if (objA === objB) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!objA || !objB) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const aKeys = Object.keys(objA)
|
|
||||||
const bKeys = Object.keys(objB)
|
|
||||||
|
|
||||||
if (bKeys.length !== aKeys.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objA instanceof Date && objB instanceof Date) {
|
|
||||||
return objA.getTime() === objB.getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aKeys.length === 0) {
|
|
||||||
return objA === objB
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < aKeys.length; i++) {
|
|
||||||
const key = aKeys[i]
|
|
||||||
|
|
||||||
if (objA[key] !== objB[key]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a string, convert snake_case to camelCase
|
|
||||||
* @param {String} string
|
|
||||||
*/
|
|
||||||
export function snakeToCamel (string: string | any): string | any {
|
|
||||||
if (typeof string === 'string') {
|
|
||||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
|
||||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
|
||||||
return $1.toUpperCase().replace('_', '')
|
|
||||||
}
|
|
||||||
return $1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the rule name with the applicable modifier as an array.
|
|
||||||
*/
|
|
||||||
function parseModifier (ruleName: any): [string|any, string|null] {
|
|
||||||
if (typeof ruleName === 'string' && /^[\^]/.test(ruleName.charAt(0))) {
|
|
||||||
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
|
||||||
}
|
|
||||||
return [snakeToCamel(ruleName), null]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts to array.
|
|
||||||
* If given parameter is not string, object ot array, result will be an empty array.
|
|
||||||
* @param {*} item
|
|
||||||
*/
|
|
||||||
export function arrayify (item: any): any[] {
|
|
||||||
if (!item) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
return [item]
|
|
||||||
}
|
|
||||||
if (Array.isArray(item)) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
if (typeof item === 'object') {
|
|
||||||
return Object.values(item)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a string or function, parse it and return an array in the format
|
|
||||||
* [fn, [...arguments]]
|
|
||||||
*/
|
|
||||||
function parseRule (rule: any, rules: Record<string, any>) {
|
|
||||||
if (typeof rule === 'function') {
|
|
||||||
return [rule, []]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(rule) && rule.length) {
|
|
||||||
rule = rule.slice() // light clone
|
|
||||||
const [ruleName, modifier] = parseModifier(rule.shift())
|
|
||||||
if (typeof ruleName === 'string' && Object.prototype.hasOwnProperty.call(rules, ruleName)) {
|
|
||||||
return [rules[ruleName], rule, ruleName, modifier]
|
|
||||||
}
|
|
||||||
if (typeof ruleName === 'function') {
|
|
||||||
return [ruleName, rule, ruleName, modifier]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof rule === 'string') {
|
|
||||||
const segments = rule.split(':')
|
|
||||||
const [ruleName, modifier] = parseModifier(segments.shift())
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(rules, ruleName)) {
|
|
||||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown validation rule ${rule}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an array or string return an array of callables.
|
|
||||||
* @param {array|string} validation
|
|
||||||
* @param {array} rules and array of functions
|
|
||||||
* @return {array} an array of functions
|
|
||||||
*/
|
|
||||||
export function parseRules (validation: any[]|string, rules: any): any[] {
|
|
||||||
if (typeof validation === 'string') {
|
|
||||||
return parseRules(validation.split('|').filter(f => f.length), rules)
|
|
||||||
}
|
|
||||||
if (!Array.isArray(validation)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return validation.map(rule => {
|
|
||||||
return parseRule(rule, rules)
|
|
||||||
}).filter(f => !!f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape a string for use in regular expressions.
|
|
||||||
*/
|
|
||||||
export function escapeRegExp (string: string): string {
|
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a string format (date) return a regex to match against.
|
|
||||||
*/
|
|
||||||
export function regexForFormat (format: string): RegExp {
|
|
||||||
const escaped = `^${escapeRegExp(format)}$`
|
|
||||||
const formats: Record<string, string> = {
|
|
||||||
MM: '(0[1-9]|1[012])',
|
|
||||||
M: '([1-9]|1[012])',
|
|
||||||
DD: '([012][1-9]|3[01])',
|
|
||||||
D: '([012]?[1-9]|3[01])',
|
|
||||||
YYYY: '\\d{4}',
|
|
||||||
YY: '\\d{2}'
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
|
||||||
return regex.replace(format, formats[format])
|
|
||||||
}, escaped))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if
|
|
||||||
* @param {*} data
|
|
||||||
*/
|
|
||||||
export function isScalar (data: any): boolean {
|
|
||||||
switch (typeof data) {
|
|
||||||
case 'symbol':
|
|
||||||
case 'number':
|
|
||||||
case 'string':
|
|
||||||
case 'boolean':
|
|
||||||
case 'undefined':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return data === null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple (somewhat non-comprehensive) cloneDeep function, valid for our use
|
|
||||||
* case of needing to unbind reactive watchers.
|
|
||||||
*/
|
|
||||||
export function cloneDeep (value: any): any {
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
|
|
||||||
|
|
||||||
for (const key in value) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
||||||
if (isScalar(value[key])) {
|
|
||||||
copy[key] = value[key]
|
|
||||||
} else {
|
|
||||||
copy[key] = cloneDeep(value[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a locale string, parse the options.
|
|
||||||
* @param {string} locale
|
|
||||||
*/
|
|
||||||
export function parseLocale (locale: string): string[] {
|
|
||||||
const segments = locale.split('-')
|
|
||||||
return segments.reduce((options: string[], segment: string) => {
|
|
||||||
if (options.length) {
|
|
||||||
options.unshift(`${options[0]}-${segment}`)
|
|
||||||
}
|
|
||||||
return options.length ? options : [segment]
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
|
||||||
*/
|
|
||||||
export function has (ctx: Record<string, any>, prop: string): boolean {
|
|
||||||
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNested (obj: Record<string, any>, field: string): any {
|
|
||||||
const fieldParts = field.split('.')
|
|
||||||
|
|
||||||
let result: Record<string, any> = obj
|
|
||||||
|
|
||||||
for (const key in fieldParts) {
|
|
||||||
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
|
|
||||||
if (result === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (matches) {
|
|
||||||
result = result[matches[1]]
|
|
||||||
|
|
||||||
if (result === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
result = result[matches[2]]
|
|
||||||
} else {
|
|
||||||
result = result[fieldParts[key]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setNested (obj: Record<string, any>, field: string, value: any): void {
|
|
||||||
const fieldParts = field.split('.')
|
|
||||||
|
|
||||||
let subProxy: Record<string, any> = obj
|
|
||||||
for (let i = 0; i < fieldParts.length; i++) {
|
|
||||||
const fieldPart = fieldParts[i]
|
|
||||||
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
if (subProxy[matches[1]] === undefined) {
|
|
||||||
subProxy[matches[1]] = []
|
|
||||||
}
|
|
||||||
subProxy = subProxy[matches[1]]
|
|
||||||
|
|
||||||
if (i === fieldParts.length - 1) {
|
|
||||||
subProxy[matches[2]] = value
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
subProxy = subProxy[matches[2]]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (i === fieldParts.length - 1) {
|
|
||||||
subProxy[fieldPart] = value
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line max-depth
|
|
||||||
if (subProxy[fieldPart] === undefined) {
|
|
||||||
subProxy[fieldPart] = {}
|
|
||||||
}
|
|
||||||
subProxy = subProxy[fieldPart]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
6
src/shims-ext.d.ts
vendored
6
src/shims-ext.d.ts
vendored
@ -11,10 +11,4 @@ declare module 'vue/types/vue' {
|
|||||||
interface VueRoute {
|
interface VueRoute {
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormularioForm extends Vue {
|
|
||||||
name: string | boolean;
|
|
||||||
proxy: Record<string, any>;
|
|
||||||
hasValidationErrors(): Promise<boolean>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
20
src/utils/arrayify.ts
Normal file
20
src/utils/arrayify.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Converts to array.
|
||||||
|
* If given parameter is not string, object ot array, result will be an empty array.
|
||||||
|
* @param {*} item
|
||||||
|
*/
|
||||||
|
export default function arrayify (item: any): any[] {
|
||||||
|
if (!item) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return [item]
|
||||||
|
}
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
return Object.values(item)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
22
src/utils/clone.ts
Normal file
22
src/utils/clone.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import isScalar from '@/utils/isScalar'
|
||||||
|
import has from '@/utils/has'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple (somewhat non-comprehensive) clone function, valid for our use
|
||||||
|
* case of needing to unbind reactive watchers.
|
||||||
|
*/
|
||||||
|
export default function clone (value: any): any {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
|
||||||
|
|
||||||
|
for (const key in value) {
|
||||||
|
if (has(value, key)) {
|
||||||
|
copy[key] = isScalar(value[key]) ? value[key] : clone(value[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy
|
||||||
|
}
|
6
src/utils/has.ts
Normal file
6
src/utils/has.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||||
|
*/
|
||||||
|
export default function has (ctx: Record<string, any>|any[], prop: string|number): boolean {
|
||||||
|
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||||
|
}
|
67
src/utils/index.ts
Normal file
67
src/utils/index.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
export { default as arrayify } from './arrayify'
|
||||||
|
export { default as clone } from './clone'
|
||||||
|
export { default as has } from './has'
|
||||||
|
export { default as isScalar } from './isScalar'
|
||||||
|
export { default as merge } from './merge'
|
||||||
|
export { default as regexForFormat } from './regexForFormat'
|
||||||
|
export { default as shallowEqualObjects } from './shallowEqualObjects'
|
||||||
|
export { default as snakeToCamel } from './snakeToCamel'
|
||||||
|
|
||||||
|
export function getNested (obj: Record<string, any>, field: string): any {
|
||||||
|
const fieldParts = field.split('.')
|
||||||
|
|
||||||
|
let result: Record<string, any> = obj
|
||||||
|
|
||||||
|
for (const key in fieldParts) {
|
||||||
|
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
|
||||||
|
if (result === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
result = result[matches[1]]
|
||||||
|
|
||||||
|
if (result === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
result = result[matches[2]]
|
||||||
|
} else {
|
||||||
|
result = result[fieldParts[key]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNested (obj: Record<string, any>, field: string, value: any): void {
|
||||||
|
const fieldParts = field.split('.')
|
||||||
|
|
||||||
|
let subProxy: Record<string, any> = obj
|
||||||
|
for (let i = 0; i < fieldParts.length; i++) {
|
||||||
|
const fieldPart = fieldParts[i]
|
||||||
|
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
if (subProxy[matches[1]] === undefined) {
|
||||||
|
subProxy[matches[1]] = []
|
||||||
|
}
|
||||||
|
subProxy = subProxy[matches[1]]
|
||||||
|
|
||||||
|
if (i === fieldParts.length - 1) {
|
||||||
|
subProxy[matches[2]] = value
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
subProxy = subProxy[matches[2]]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i === fieldParts.length - 1) {
|
||||||
|
subProxy[fieldPart] = value
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line max-depth
|
||||||
|
if (subProxy[fieldPart] === undefined) {
|
||||||
|
subProxy[fieldPart] = {}
|
||||||
|
}
|
||||||
|
subProxy = subProxy[fieldPart]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/utils/isScalar.ts
Normal file
12
src/utils/isScalar.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default function isScalar (data: any): boolean {
|
||||||
|
switch (typeof data) {
|
||||||
|
case 'symbol':
|
||||||
|
case 'number':
|
||||||
|
case 'string':
|
||||||
|
case 'boolean':
|
||||||
|
case 'undefined':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return data === null
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import { has } from '@/libs/utils.ts'
|
import has from '@/utils/has.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new object by copying properties of base and mergeWith.
|
* Create a new object by copying properties of base and mergeWith.
|
||||||
|
25
src/utils/regexForFormat.ts
Normal file
25
src/utils/regexForFormat.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Escape a string for use in regular expressions.
|
||||||
|
*/
|
||||||
|
function escapeRegExp (string: string): string {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string format (date) return a regex to match against.
|
||||||
|
*/
|
||||||
|
export default function regexForFormat (format: string): RegExp {
|
||||||
|
const escaped = `^${escapeRegExp(format)}$`
|
||||||
|
const formats: Record<string, string> = {
|
||||||
|
MM: '(0[1-9]|1[012])',
|
||||||
|
M: '([1-9]|1[012])',
|
||||||
|
DD: '([012][1-9]|3[01])',
|
||||||
|
D: '([012]?[1-9]|3[01])',
|
||||||
|
YYYY: '\\d{4}',
|
||||||
|
YY: '\\d{2}'
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||||
|
return regex.replace(format, formats[format])
|
||||||
|
}, escaped))
|
||||||
|
}
|
34
src/utils/shallowEqualObjects.ts
Normal file
34
src/utils/shallowEqualObjects.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export default function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
||||||
|
if (objA === objB) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!objA || !objB) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const aKeys = Object.keys(objA)
|
||||||
|
const bKeys = Object.keys(objB)
|
||||||
|
|
||||||
|
if (bKeys.length !== aKeys.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objA instanceof Date && objB instanceof Date) {
|
||||||
|
return objA.getTime() === objB.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aKeys.length === 0) {
|
||||||
|
return objA === objB
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < aKeys.length; i++) {
|
||||||
|
const key = aKeys[i]
|
||||||
|
|
||||||
|
if (objA[key] !== objB[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
11
src/utils/snakeToCamel.ts
Normal file
11
src/utils/snakeToCamel.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Given a string, convert snake_case to camelCase
|
||||||
|
*/
|
||||||
|
export default function snakeToCamel (string: string): string {
|
||||||
|
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||||
|
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||||
|
return $1.toUpperCase().replace('_', '')
|
||||||
|
}
|
||||||
|
return $1
|
||||||
|
})
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { has } from '@/libs/utils'
|
import { has } from '@/utils'
|
||||||
|
|
||||||
export interface ErrorHandler {
|
export interface ErrorHandler {
|
||||||
(errors: Record<string, any> | any[]): void;
|
(errors: Record<string, any> | any[]): void;
|
||||||
|
@ -1,19 +1,5 @@
|
|||||||
import { ValidationContext } from '@/validation/types'
|
import { ValidationContext } from '@/validation/validator'
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an object of functions that each produce valid responses. There's no
|
|
||||||
* need for these to be 1-1 with english, feel free to change the wording or
|
|
||||||
* use/not use any of the variables available in the object or the
|
|
||||||
* arguments for the message to make the most sense in your language and culture.
|
|
||||||
*
|
|
||||||
* The validation context object includes the following properties:
|
|
||||||
* {
|
|
||||||
* args // Array of rule arguments: between:5,10 (args are ['5', '10'])
|
|
||||||
* name: // The validation name to be used
|
|
||||||
* value: // The value of the field (do not mutate!),
|
|
||||||
* formValues: // If wrapped in a FormulateForm, the value of other form fields.
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* The default render method for error messages.
|
* The default render method for error messages.
|
||||||
@ -32,8 +18,8 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* The date is not after.
|
* The date is not after.
|
||||||
*/
|
*/
|
||||||
after (vm: Vue, context: ValidationContext): string {
|
after (vm: Vue, context: ValidationContext, compare: string | false = false): string {
|
||||||
if (Array.isArray(context.args) && context.args.length) {
|
if (typeof compare === 'string' && compare.length) {
|
||||||
return vm.$t('validation.after.compare', context)
|
return vm.$t('validation.after.compare', context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,15 +36,15 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* Rule: checks if the value is alpha numeric
|
* Rule: checks if the value is alpha numeric
|
||||||
*/
|
*/
|
||||||
alphanumeric (vm: Vue, context: Record<string, any>): string {
|
alphanumeric (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.alphanumeric', context)
|
return vm.$t('validation.alphanumeric', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date is not before.
|
* The date is not before.
|
||||||
*/
|
*/
|
||||||
before (vm: Vue, context: ValidationContext): string {
|
before (vm: Vue, context: ValidationContext, compare: string|false = false): string {
|
||||||
if (Array.isArray(context.args) && context.args.length) {
|
if (typeof compare === 'string' && compare.length) {
|
||||||
return vm.$t('validation.before.compare', context)
|
return vm.$t('validation.before.compare', context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,14 +54,14 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* The value is not between two numbers or lengths
|
* The value is not between two numbers or lengths
|
||||||
*/
|
*/
|
||||||
between (vm: Vue, context: ValidationContext): string {
|
between (vm: Vue, context: ValidationContext, from: number|any = 0, to: number|any = 10, force?: string): string {
|
||||||
const force = Array.isArray(context.args) && context.args[2] ? context.args[2] : false
|
const data = { ...context, from, to }
|
||||||
|
|
||||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||||
return vm.$t('validation.between.force', context)
|
return vm.$t('validation.between.force', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm.$t('validation.between.default', context)
|
return vm.$t('validation.between.default', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,8 +74,8 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* Is not a valid date.
|
* Is not a valid date.
|
||||||
*/
|
*/
|
||||||
date (vm: Vue, context: ValidationContext): string {
|
date (vm: Vue, context: ValidationContext, format: string | false = false): string {
|
||||||
if (Array.isArray(context.args) && context.args.length) {
|
if (typeof format === 'string' && format.length) {
|
||||||
return vm.$t('validation.date.format', context)
|
return vm.$t('validation.date.format', context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,45 +117,30 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* The maximum value allowed.
|
* The maximum value allowed.
|
||||||
*/
|
*/
|
||||||
max (vm: Vue, context: ValidationContext): string {
|
max (vm: Vue, context: ValidationContext, maximum: string | number = 10, force?: string): string {
|
||||||
const maximum = context.args[0] as number
|
|
||||||
|
|
||||||
if (Array.isArray(context.value)) {
|
if (Array.isArray(context.value)) {
|
||||||
return vm.$tc('validation.max.array', maximum, context)
|
return vm.$tc('validation.max.array', maximum, context)
|
||||||
}
|
}
|
||||||
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
|
|
||||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||||
return vm.$tc('validation.max.force', maximum, context)
|
return vm.$tc('validation.max.force', maximum, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm.$tc('validation.max.default', maximum, context)
|
return vm.$tc('validation.max.default', maximum, context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* The (field-level) error message for mime errors.
|
|
||||||
*/
|
|
||||||
mime (vm: Vue, context: ValidationContext): string {
|
|
||||||
const types = context.args[0]
|
|
||||||
|
|
||||||
if (types) {
|
|
||||||
return vm.$t('validation.mime.default', context)
|
|
||||||
} else {
|
|
||||||
return vm.$t('validation.mime.no_formats_allowed', context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum value allowed.
|
* The maximum value allowed.
|
||||||
*/
|
*/
|
||||||
min (vm: Vue, context: ValidationContext): string {
|
min (vm: Vue, context: ValidationContext, minimum: number | any = 1, force?: string): string {
|
||||||
const minimum = context.args[0] as number
|
|
||||||
|
|
||||||
if (Array.isArray(context.value)) {
|
if (Array.isArray(context.value)) {
|
||||||
return vm.$tc('validation.min.array', minimum, context)
|
return vm.$tc('validation.min.array', minimum, context)
|
||||||
}
|
}
|
||||||
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
|
|
||||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||||
return vm.$tc('validation.min.force', minimum, context)
|
return vm.$tc('validation.min.force', minimum, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm.$tc('validation.min.default', minimum, context)
|
return vm.$tc('validation.min.default', minimum, context)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import isUrl from 'is-url'
|
import isUrl from 'is-url'
|
||||||
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
import { shallowEqualObjects, regexForFormat, has } from '@/utils'
|
||||||
import { ValidatableData } from '@/validation/types'
|
import { ValidationContext } from '@/validation/validator'
|
||||||
|
|
||||||
|
interface DateValidationContext extends ValidationContext {
|
||||||
|
value: Date|string;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* Rule: the value must be "yes", "on", "1", or true
|
* Rule: the value must be "yes", "on", "1", or true
|
||||||
*/
|
*/
|
||||||
accepted ({ value }: ValidatableData): Promise<boolean> {
|
accepted ({ value }: ValidationContext): Promise<boolean> {
|
||||||
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
|
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule: checks if a value is after a given date. Defaults to current time
|
* Rule: checks if a value is after a given date. Defaults to current time
|
||||||
*/
|
*/
|
||||||
after ({ value }: { value: Date|string }, compare: string | false = false): Promise<boolean> {
|
after ({ value }: DateValidationContext, compare: string | false = false): Promise<boolean> {
|
||||||
const timestamp = compare !== false ? Date.parse(compare) : Date.now()
|
const timestamp = compare !== false ? Date.parse(compare) : Date.now()
|
||||||
const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
|
const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
|
||||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
||||||
@ -23,12 +27,12 @@ export default {
|
|||||||
* Rule: checks if the value is only alpha
|
* Rule: checks if the value is only alpha
|
||||||
*/
|
*/
|
||||||
alpha ({ value }: { value: string }, set = 'default'): Promise<boolean> {
|
alpha ({ value }: { value: string }, set = 'default'): Promise<boolean> {
|
||||||
const sets = {
|
const sets: Record<string, RegExp> = {
|
||||||
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
||||||
latin: /^[a-zA-Z]+$/
|
latin: /^[a-zA-Z]+$/
|
||||||
}
|
}
|
||||||
const selectedSet = has(sets, set) ? set : 'default'
|
const selectedSet = has(sets, set) ? set : 'default'
|
||||||
// @ts-ignore
|
|
||||||
return Promise.resolve(sets[selectedSet].test(value))
|
return Promise.resolve(sets[selectedSet].test(value))
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -36,19 +40,19 @@ export default {
|
|||||||
* Rule: checks if the value is alpha numeric
|
* Rule: checks if the value is alpha numeric
|
||||||
*/
|
*/
|
||||||
alphanumeric ({ value }: { value: string }, set = 'default'): Promise<boolean> {
|
alphanumeric ({ value }: { value: string }, set = 'default'): Promise<boolean> {
|
||||||
const sets = {
|
const sets: Record<string, RegExp> = {
|
||||||
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
|
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
|
||||||
latin: /^[a-zA-Z0-9]+$/
|
latin: /^[a-zA-Z0-9]+$/
|
||||||
}
|
}
|
||||||
const selectedSet = has(sets, set) ? set : 'default'
|
const selectedSet = has(sets, set) ? set : 'default'
|
||||||
// @ts-ignore
|
|
||||||
return Promise.resolve(sets[selectedSet].test(value))
|
return Promise.resolve(sets[selectedSet].test(value))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule: checks if a value is after a given date. Defaults to current time
|
* Rule: checks if a value is after a given date. Defaults to current time
|
||||||
*/
|
*/
|
||||||
before ({ value }: { value: Date|string }, compare: string|false = false): Promise<boolean> {
|
before ({ value }: DateValidationContext, compare: string|false = false): Promise<boolean> {
|
||||||
const timestamp = compare !== false ? Date.parse(compare) : Date.now()
|
const timestamp = compare !== false ? Date.parse(compare) : Date.now()
|
||||||
const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
|
const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
|
||||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
|
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
|
||||||
@ -80,13 +84,13 @@ export default {
|
|||||||
* Confirm that the value of one field is the same as another, mostly used
|
* Confirm that the value of one field is the same as another, mostly used
|
||||||
* for password confirmations.
|
* for password confirmations.
|
||||||
*/
|
*/
|
||||||
confirm ({ value, getFormValues, name }: ValidatableData, field?: string): Promise<boolean> {
|
confirm ({ value, formValues, name }: ValidationContext, field?: string): Promise<boolean> {
|
||||||
return Promise.resolve(((): boolean => {
|
return Promise.resolve(((): boolean => {
|
||||||
let confirmationFieldName = field
|
let confirmationFieldName = field
|
||||||
if (!confirmationFieldName) {
|
if (!confirmationFieldName) {
|
||||||
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
||||||
}
|
}
|
||||||
return getFormValues()[confirmationFieldName] === value
|
return formValues[confirmationFieldName] === value
|
||||||
})())
|
})())
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -150,27 +154,6 @@ export default {
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the minimum value of a particular.
|
|
||||||
*/
|
|
||||||
min ({ value }: { value: any }, minimum: number | any = 1, force?: string): Promise<boolean> {
|
|
||||||
return Promise.resolve(((): boolean => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
minimum = !isNaN(minimum) ? Number(minimum) : minimum
|
|
||||||
return value.length >= minimum
|
|
||||||
}
|
|
||||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
|
||||||
value = !isNaN(value) ? Number(value) : value
|
|
||||||
return value >= minimum
|
|
||||||
}
|
|
||||||
if (typeof value === 'string' || (force === 'length')) {
|
|
||||||
value = !isNaN(value) ? value.toString() : value
|
|
||||||
return value.length >= minimum
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})())
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the maximum value of a particular.
|
* Check the maximum value of a particular.
|
||||||
*/
|
*/
|
||||||
@ -192,6 +175,27 @@ export default {
|
|||||||
})())
|
})())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the minimum value of a particular.
|
||||||
|
*/
|
||||||
|
min ({ value }: { value: any }, minimum: number | any = 1, force?: string): Promise<boolean> {
|
||||||
|
return Promise.resolve(((): boolean => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
minimum = !isNaN(minimum) ? Number(minimum) : minimum
|
||||||
|
return value.length >= minimum
|
||||||
|
}
|
||||||
|
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||||
|
value = !isNaN(value) ? Number(value) : value
|
||||||
|
return value >= minimum
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' || (force === 'length')) {
|
||||||
|
value = !isNaN(value) ? value.toString() : value
|
||||||
|
return value.length >= minimum
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})())
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule: Value is not in stack.
|
* Rule: Value is not in stack.
|
||||||
*/
|
*/
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
export interface ValidatableData {
|
|
||||||
// The value of the field (do not mutate!),
|
|
||||||
value: any;
|
|
||||||
// If wrapped in a FormulateForm, the value of other form fields.
|
|
||||||
getFormValues(): Record<string, any>;
|
|
||||||
// The validation name to be used
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationContext {
|
|
||||||
// The value of the field (do not mutate!),
|
|
||||||
value: any;
|
|
||||||
// If wrapped in a FormulateForm, the value of other form fields.
|
|
||||||
formValues: Record<string, any>;
|
|
||||||
// The validation name to be used
|
|
||||||
name: string;
|
|
||||||
// Array of rule arguments: between:5,10 (args are ['5', '10'])
|
|
||||||
args: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationRule {
|
|
||||||
(context: ValidatableData, ...args: any[]): Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationError {
|
|
||||||
rule?: string;
|
|
||||||
context?: ValidationContext;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationErrorBag {
|
|
||||||
name: string;
|
|
||||||
errors: ValidationError[];
|
|
||||||
}
|
|
@ -1,12 +1,31 @@
|
|||||||
import {
|
import { has, snakeToCamel } from '@/utils'
|
||||||
ValidatableData,
|
|
||||||
ValidationRule,
|
|
||||||
} from '@/validation/types'
|
|
||||||
|
|
||||||
export type Validator = {
|
export interface Validator {
|
||||||
name?: string;
|
(context: ValidationContext): Promise<Violation|null>;
|
||||||
rule: ValidationRule;
|
}
|
||||||
|
|
||||||
|
export interface Violation {
|
||||||
|
rule: string|null;
|
||||||
args: any[];
|
args: any[];
|
||||||
|
context: ValidationContext|null;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckRuleFn {
|
||||||
|
(context: ValidationContext, ...args: any[]): Promise<boolean>|boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMessageFn {
|
||||||
|
(context: ValidationContext, ...args: any[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationContext {
|
||||||
|
// The value of the field (do not mutate!),
|
||||||
|
value: any;
|
||||||
|
// If wrapped in a FormulateForm, the value of other form fields.
|
||||||
|
formValues: Record<string, any>;
|
||||||
|
// The validation name to be used
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidatorGroup = {
|
export type ValidatorGroup = {
|
||||||
@ -14,6 +33,124 @@ export type ValidatorGroup = {
|
|||||||
bail: boolean;
|
bail: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createValidator (
|
||||||
|
ruleFn: CheckRuleFn,
|
||||||
|
ruleName: string|null,
|
||||||
|
ruleArgs: any[],
|
||||||
|
messageFn: CreateMessageFn
|
||||||
|
): Validator {
|
||||||
|
return (context: ValidationContext): Promise<Violation|null> => {
|
||||||
|
return Promise.resolve(ruleFn(context, ...ruleArgs))
|
||||||
|
.then(valid => {
|
||||||
|
return !valid ? {
|
||||||
|
rule: ruleName,
|
||||||
|
args: ruleArgs,
|
||||||
|
context,
|
||||||
|
message: messageFn(context, ...ruleArgs),
|
||||||
|
} : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseModifier (ruleName: string): [string, string|null] {
|
||||||
|
if (/^[\^]/.test(ruleName.charAt(0))) {
|
||||||
|
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
||||||
|
}
|
||||||
|
return [snakeToCamel(ruleName), null]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processSingleArrayConstraint (
|
||||||
|
constraint: any[],
|
||||||
|
rules: Record<string, CheckRuleFn>,
|
||||||
|
messages: Record<string, CreateMessageFn>
|
||||||
|
): [Validator, string|null, string|null] {
|
||||||
|
const args = constraint.slice()
|
||||||
|
const first = args.shift()
|
||||||
|
|
||||||
|
if (typeof first === 'function') {
|
||||||
|
return [first, null, null]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof first !== 'string') {
|
||||||
|
throw new Error('[Formulario]: For array constraint first element must be rule name or Validator function')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name, modifier] = parseModifier(first)
|
||||||
|
|
||||||
|
if (has(rules, name)) {
|
||||||
|
return [
|
||||||
|
createValidator(
|
||||||
|
rules[name],
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
messages[name] || messages.default
|
||||||
|
),
|
||||||
|
name,
|
||||||
|
modifier,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`[Formulario] Can't create validator for constraint: ${JSON.stringify(constraint)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processSingleStringConstraint (
|
||||||
|
constraint: string,
|
||||||
|
rules: Record<string, CheckRuleFn>,
|
||||||
|
messages: Record<string, CreateMessageFn>
|
||||||
|
): [Validator, string|null, string|null] {
|
||||||
|
const args = constraint.split(':')
|
||||||
|
const [name, modifier] = parseModifier(args.shift() || '')
|
||||||
|
|
||||||
|
if (has(rules, name)) {
|
||||||
|
return [
|
||||||
|
createValidator(
|
||||||
|
rules[name],
|
||||||
|
name,
|
||||||
|
args.length ? args.join(':').split(',') : [],
|
||||||
|
messages[name] || messages.default
|
||||||
|
),
|
||||||
|
name,
|
||||||
|
modifier,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`[Formulario] Can't create validator for constraint: ${constraint}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processSingleConstraint (
|
||||||
|
constraint: string|Validator|[Validator|string, ...any[]],
|
||||||
|
rules: Record<string, CheckRuleFn>,
|
||||||
|
messages: Record<string, CreateMessageFn>
|
||||||
|
): [Validator, string|null, string|null] {
|
||||||
|
if (typeof constraint === 'function') {
|
||||||
|
return [constraint, null, null]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(constraint) && constraint.length) {
|
||||||
|
return processSingleArrayConstraint(constraint, rules, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof constraint === 'string') {
|
||||||
|
return processSingleStringConstraint(constraint, rules, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(): Promise<Violation|null> => Promise.resolve(null), null, null]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processConstraints (
|
||||||
|
constraints: string|any[],
|
||||||
|
rules: Record<string, CheckRuleFn>,
|
||||||
|
messages: Record<string, CreateMessageFn>
|
||||||
|
): [Validator, string|null, string|null][] {
|
||||||
|
if (typeof constraints === 'string') {
|
||||||
|
return processConstraints(constraints.split('|').filter(f => f.length), rules, messages)
|
||||||
|
}
|
||||||
|
if (!Array.isArray(constraints)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return constraints.map(constraint => processSingleConstraint(constraint, rules, messages))
|
||||||
|
}
|
||||||
|
|
||||||
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
||||||
const enlarged: ValidatorGroup[] = []
|
const enlarged: ValidatorGroup[] = []
|
||||||
|
|
||||||
@ -46,25 +183,20 @@ export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
|||||||
* [[required, min, max]]
|
* [[required, min, max]]
|
||||||
* @param {array} rules
|
* @param {array} rules
|
||||||
*/
|
*/
|
||||||
export function createValidatorGroups (rules: [ValidationRule, any[], string, string|null][]): ValidatorGroup[] {
|
export function createValidatorGroups (rules: [Validator, string|null, string|null][]): ValidatorGroup[] {
|
||||||
const mapper = ([
|
const mapper = ([validator, /** name */, modifier]: [Validator, string|null, string|null]): ValidatorGroup => ({
|
||||||
rule,
|
validators: [validator],
|
||||||
args,
|
|
||||||
name,
|
|
||||||
modifier
|
|
||||||
]: [ValidationRule, any[], string, any]): ValidatorGroup => ({
|
|
||||||
validators: [{ name, rule, args }],
|
|
||||||
bail: modifier === '^',
|
bail: modifier === '^',
|
||||||
})
|
})
|
||||||
|
|
||||||
const groups: ValidatorGroup[] = []
|
const groups: ValidatorGroup[] = []
|
||||||
|
|
||||||
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
const bailIndex = rules.findIndex(([, name]) => name && name.toLowerCase() === 'bail')
|
||||||
|
|
||||||
if (bailIndex >= 0) {
|
if (bailIndex >= 0) {
|
||||||
groups.push(...enlarge(rules.splice(0, bailIndex + 1).slice(0, -1).map(mapper)))
|
groups.push(...enlarge(rules.splice(0, bailIndex + 1).slice(0, -1).map(mapper)))
|
||||||
groups.push(...rules.map(([rule, args, name]) => ({
|
groups.push(...rules.map(([validator]) => ({
|
||||||
validators: [{ rule, args, name }],
|
validators: [validator],
|
||||||
bail: true,
|
bail: true,
|
||||||
})))
|
})))
|
||||||
} else {
|
} else {
|
||||||
@ -74,6 +206,33 @@ export function createValidatorGroups (rules: [ValidationRule, any[], string, st
|
|||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validate (validator: Validator, data: ValidatableData): Promise<boolean> {
|
function validateByGroup (group: ValidatorGroup, context: ValidationContext): Promise<Violation[]> {
|
||||||
return Promise.resolve(validator.rule(data, ...validator.args))
|
return Promise.all(
|
||||||
|
group.validators.map(validate => validate(context))
|
||||||
|
)
|
||||||
|
.then(violations => (violations.filter(v => v !== null) as Violation[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate (
|
||||||
|
validators: [Validator, string|null, string|null][],
|
||||||
|
context: ValidationContext
|
||||||
|
): Promise<Violation[]> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const resolveGroups = (groups: ValidatorGroup[], all: Violation[] = []): void => {
|
||||||
|
if (groups.length) {
|
||||||
|
const current = groups.shift() as ValidatorGroup
|
||||||
|
|
||||||
|
validateByGroup(current, context).then(violations => {
|
||||||
|
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
||||||
|
if ((violations.length === 0 || !current.bail) && groups.length) {
|
||||||
|
return resolveGroups(groups, all.concat(violations))
|
||||||
|
}
|
||||||
|
return resolve(all.concat(violations))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolveGroups(createValidatorGroups(validators))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="text-danger"
|
class="text-danger"
|
||||||
>
|
>
|
||||||
{{ error.message }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
|
|
||||||
@ -46,7 +46,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="text-danger"
|
class="text-danger"
|
||||||
>
|
>
|
||||||
{{ error.message }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioGrouping>
|
</FormularioGrouping>
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="text-danger"
|
class="text-danger"
|
||||||
>
|
>
|
||||||
{{ error.message }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ describe('FormularioForm', () => {
|
|||||||
propsData: { formularioValue: { test: 'has initial value' } },
|
propsData: { formularioValue: { test: 'has initial value' } },
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioInput v-slot="{ context }" formulario-value="123" name="test" >
|
<FormularioInput v-slot="{ context }" name="test" value="123">
|
||||||
<input v-model="context.model" type="text">
|
<input v-model="context.model" type="text">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
`
|
`
|
||||||
@ -164,30 +164,39 @@ describe('FormularioForm', () => {
|
|||||||
it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
|
it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: {
|
propsData: {
|
||||||
formularioValue: { test: '123' }
|
formularioValue: { test: 'Initial' }
|
||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
default: '<FormularioInput name="test" formulario-value="override-data" />'
|
default: '<FormularioInput name="test" value="Overrides" />'
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
expect(wrapper.emitted().input[wrapper.emitted().input.length - 1]).toEqual([{ test: 'override-data' }])
|
|
||||||
|
const emitted = wrapper.emitted('input')
|
||||||
|
|
||||||
|
expect(emitted).toBeTruthy()
|
||||||
|
expect(emitted[emitted.length - 1]).toEqual([{ test: 'Overrides' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates an inputs value when the form v-model is modified', async () => {
|
it('updates an inputs value when the form v-model is modified', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data: () => ({ formValues: { test: 'abcd' } }),
|
data: () => ({ values: { test: 'abcd' } }),
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm v-model="formValues">
|
<FormularioForm v-model="values">
|
||||||
<FormularioInput v-slot="{ context }" name="test" >
|
<FormularioInput v-slot="{ context }" name="test" >
|
||||||
<input v-model="context.model" type="text">
|
<input v-model="context.model" type="text">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
wrapper.vm.values = { test: '1234' }
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
wrapper.vm.formValues = { test: '1234' }
|
|
||||||
await flushPromises()
|
const input = wrapper.find('input[type="text"]')
|
||||||
expect(wrapper.find('input[type="text"]').element['value']).toBe('1234')
|
|
||||||
|
expect(input).toBeTruthy()
|
||||||
|
expect(input.element['value']).toBe('1234')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Resolves hasValidationErrors to true', async () => {
|
it('Resolves hasValidationErrors to true', async () => {
|
||||||
@ -242,7 +251,7 @@ describe('FormularioForm', () => {
|
|||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioInput v-slot="{ context }" name="fieldWithErrors">
|
<FormularioInput v-slot="{ context }" name="fieldWithErrors">
|
||||||
<span v-for="error in context.allErrors">{{ error.message }}</span>
|
<span v-for="error in context.errors">{{ error }}</span>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -289,7 +298,7 @@ describe('FormularioForm', () => {
|
|||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioInput v-slot="{ context }" name="foo" validation="required|in:foo">
|
<FormularioInput v-slot="{ context }" name="foo" validation="required|in:foo">
|
||||||
<input v-model="context.model" type="text" @blur="context.blurHandler">
|
<input v-model="context.model" type="text" @blur="context.runValidation()">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
<FormularioInput name="bar" validation="required" />
|
<FormularioInput name="bar" validation="required" />
|
||||||
`,
|
`,
|
||||||
@ -304,7 +313,7 @@ describe('FormularioForm', () => {
|
|||||||
expect(wrapper.emitted('validation').length).toBe(1)
|
expect(wrapper.emitted('validation').length).toBe(1)
|
||||||
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
errors: [],
|
violations: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -312,7 +321,7 @@ describe('FormularioForm', () => {
|
|||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: { default: `
|
slots: { default: `
|
||||||
<FormularioInput v-slot="{ context }" name="foo" validation="required|in:foo">
|
<FormularioInput v-slot="{ context }" name="foo" validation="required|in:foo">
|
||||||
<input v-model="context.model" type="text" @blur="context.blurHandler">
|
<input v-model="context.model" type="text" @blur="context.runValidation()">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
<FormularioInput name="bar" validation="required" />
|
<FormularioInput name="bar" validation="required" />
|
||||||
` }
|
` }
|
||||||
@ -326,7 +335,7 @@ describe('FormularioForm', () => {
|
|||||||
expect(wrapper.emitted('validation').length).toBe(1)
|
expect(wrapper.emitted('validation').length).toBe(1)
|
||||||
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
errors: [ expect.any(Object) ], // @TODO: Check object structure
|
violations: [ expect.any(Object) ], // @TODO: Check object structure
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -390,6 +399,6 @@ describe('FormularioForm', () => {
|
|||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(0)
|
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(0)
|
||||||
expect(wrapper.vm.values).toEqual({})
|
expect(wrapper.vm['values']).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -53,7 +53,7 @@ describe('FormularioGrouping', () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.find('input[type="text"]').element.value).toBe('Group text')
|
expect(wrapper.find('input[type="text"]').element['value']).toBe('Group text')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Data reactive with grouped fields', async () => {
|
it('Data reactive with grouped fields', async () => {
|
||||||
@ -86,7 +86,7 @@ describe('FormularioGrouping', () => {
|
|||||||
default: `
|
default: `
|
||||||
<FormularioGrouping name="group">
|
<FormularioGrouping name="group">
|
||||||
<FormularioInput ref="input" name="text" v-slot="{ context }">
|
<FormularioInput ref="input" name="text" v-slot="{ context }">
|
||||||
<span v-for="error in context.allErrors">{{ error }}</span>
|
<span v-for="error in context.errors">{{ error }}</span>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioGrouping>
|
</FormularioGrouping>
|
||||||
`,
|
`,
|
||||||
|
@ -9,34 +9,34 @@ import FormularioInput from '@/FormularioInput.vue'
|
|||||||
const globalRule = jest.fn(() => { return false })
|
const globalRule = jest.fn(() => { return false })
|
||||||
|
|
||||||
Vue.use(Formulario, {
|
Vue.use(Formulario, {
|
||||||
rules: { globalRule },
|
validationRules: { globalRule },
|
||||||
validationMessages: {
|
validationMessages: {
|
||||||
required: () => 'required',
|
required: () => 'required',
|
||||||
'in': () => 'in',
|
'in': () => 'in',
|
||||||
min: () => 'min',
|
min: () => 'min',
|
||||||
globalRule: () => 'globalRule',
|
globalRule: () => 'globalRule',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('FormularioInput', () => {
|
describe('FormularioInput', () => {
|
||||||
it('allows custom field-rule level validation strings', async () => {
|
it('Allows custom field-rule level validation strings', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
value: 'other value',
|
||||||
validation: 'required|in:abcdef',
|
validation: 'required|in:abcdef',
|
||||||
validationMessages: {in: 'the value was different than expected'},
|
validationMessages: { in: 'the value was different than expected' },
|
||||||
errorBehavior: 'live',
|
validationBehavior: 'live',
|
||||||
value: 'other value'
|
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
default: `<div><span v-for="violation in props.context.violations">{{ violation.message }}</span></div>`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('span').text()).toBe('the value was different than expected')
|
expect(wrapper.find('span').text()).toBe('the value was different than expected')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('no validation on created when errorBehavior is not live', async () => {
|
it('No validation on created when validationBehavior is not live', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
@ -45,145 +45,142 @@ describe('FormularioInput', () => {
|
|||||||
value: 'other value'
|
value: 'other value'
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
expect(wrapper.find('span').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('no validation on value change when errorBehavior is not live', async () => {
|
it('No validation on value change when validationBehavior is "submit"', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
validation: 'required|in:abcdef',
|
validation: 'required|in:abcdef',
|
||||||
validationMessages: {in: 'the value was different than expected'},
|
validationMessages: {in: 'the value was different than expected'},
|
||||||
errorBehavior: 'submit',
|
validationBehavior: 'submit',
|
||||||
value: 'other value'
|
value: 'Initial'
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div>
|
default: `<div>
|
||||||
<input type="text" v-model="props.context.model">
|
<input type="text" v-model="props.context.model">
|
||||||
<span v-for="error in props.context.allErrors">{{ error.message }}</span>
|
<span v-for="error in props.context.violations">{{ error.message }}</span>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
expect(wrapper.find('span').exists()).toBe(false)
|
||||||
|
|
||||||
const input = wrapper.find('input[type="text"]')
|
wrapper.find('input[type="text"]').element['value'] = 'Test'
|
||||||
input.element.value = 'test'
|
wrapper.find('input[type="text"]').trigger('change')
|
||||||
input.trigger('input')
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('input[type="text"]').element.value).toBe('test')
|
|
||||||
|
expect(wrapper.find('input[type="text"]').element['value']).toBe('Test')
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
expect(wrapper.find('span').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows custom field-rule level validation functions', async () => {
|
it('Allows custom field-rule level validation functions', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
validation: 'required|in:abcdef',
|
validation: 'required|in:abcdef',
|
||||||
validationMessages: { in: ({ value }) => `The string ${value} is not correct.` },
|
validationMessages: { in: ({ value }) => `The string ${value} is not correct.` },
|
||||||
errorBehavior: 'live',
|
validationBehavior: 'live',
|
||||||
value: 'other value'
|
value: 'other value'
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('span').text()).toBe('The string other value is not correct.')
|
expect(wrapper.find('span').text()).toBe('The string other value is not correct.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('no validation on created when errorBehavior is not live', async () => {
|
it('No validation on created when validationBehavior is default', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
validation: 'required|in:abcdef',
|
validation: 'required|in:abcdef',
|
||||||
validationMessages: {in: 'the value was different than expected'},
|
validationMessages: { in: 'the value was different than expected' },
|
||||||
value: 'other value'
|
value: 'other value'
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
expect(wrapper.find('span').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses custom async validation rules on defined on the field', async () => {
|
it('Uses custom async validation rules on defined on the field', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
validation: 'required|foobar',
|
validation: 'required|foobar',
|
||||||
validationMessages: {
|
validationRules: { foobar: async ({ value }) => value === 'foo' },
|
||||||
foobar: 'failed the foobar check'
|
validationMessages: { foobar: 'failed the foobar check' },
|
||||||
},
|
validationBehavior: 'live',
|
||||||
validationRules: {
|
|
||||||
foobar: async ({ value }) => value === 'foo'
|
|
||||||
},
|
|
||||||
errorBehavior: 'live',
|
|
||||||
value: 'bar'
|
value: 'bar'
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('span').text()).toBe('failed the foobar check')
|
expect(wrapper.find('span').text()).toBe('failed the foobar check')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses custom sync validation rules on defined on the field', async () => {
|
it('Uses custom sync validation rules on defined on the field', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
value: 'bar',
|
||||||
validation: 'required|foobar',
|
validation: 'required|foobar',
|
||||||
validationMessages: {
|
validationRules: { foobar: ({ value }) => value === 'foo' },
|
||||||
foobar: 'failed the foobar check'
|
validationMessages: { foobar: 'failed the foobar check' },
|
||||||
},
|
validationBehavior: 'live',
|
||||||
validationRules: {
|
|
||||||
foobar: ({ value }) => value === 'foo'
|
|
||||||
},
|
|
||||||
errorBehavior: 'live',
|
|
||||||
value: 'bar'
|
|
||||||
},
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('span').text()).toBe('failed the foobar check')
|
expect(wrapper.find('span').text()).toBe('failed the foobar check')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses global custom validation rules', async () => {
|
it('Uses global custom validation rules', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
value: 'bar',
|
||||||
validation: 'required|globalRule',
|
validation: 'required|globalRule',
|
||||||
errorBehavior: 'live',
|
validationBehavior: 'live',
|
||||||
value: 'bar'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(globalRule.mock.calls.length).toBe(1)
|
expect(globalRule.mock.calls.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits correct validation event', async () => {
|
it('Emits correct validation event', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
validation: 'required',
|
name: 'fieldName',
|
||||||
errorBehavior: 'live',
|
|
||||||
value: '',
|
value: '',
|
||||||
name: 'testinput',
|
validation: 'required',
|
||||||
|
validationBehavior: 'live',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
const errorObject = wrapper.emitted('validation')[0][0]
|
|
||||||
expect(errorObject).toEqual({
|
expect(wrapper.emitted('validation')).toBeTruthy()
|
||||||
name: 'testinput',
|
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
||||||
errors: [{
|
name: 'fieldName',
|
||||||
|
violations: [{
|
||||||
rule: expect.stringContaining('required'),
|
rule: expect.stringContaining('required'),
|
||||||
|
args: expect.any(Array),
|
||||||
context: expect.any(Object),
|
context: expect.any(Object),
|
||||||
message: expect.any(String),
|
message: expect.any(String),
|
||||||
}],
|
}],
|
||||||
@ -192,86 +189,90 @@ describe('FormularioInput', () => {
|
|||||||
|
|
||||||
it('Can bail on validation when encountering the bail rule', async () => {
|
it('Can bail on validation when encountering the bail rule', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
|
propsData: {
|
||||||
|
name: 'test',
|
||||||
|
validation: 'bail|required|in:xyz',
|
||||||
|
validationBehavior: 'live',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(wrapper.vm.context.validationErrors.length).toBe(1);
|
expect(wrapper.vm.context.violations.length).toBe(1);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can show multiple validation errors if they occur before the bail rule', async () => {
|
it('Can show multiple validation errors if they occur before the bail rule', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
|
propsData: {
|
||||||
|
name: 'test',
|
||||||
|
validation: 'required|in:xyz|bail',
|
||||||
|
validationBehavior: 'live',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(wrapper.vm.context.validationErrors.length).toBe(2);
|
expect(wrapper.vm.context.violations.length).toBe(2);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can avoid bail behavior by using modifier', async () => {
|
it('Can avoid bail behavior by using modifier', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' }
|
propsData: {
|
||||||
|
name: 'test',
|
||||||
|
value: '123',
|
||||||
|
validation: '^required|in:xyz|min:10,length',
|
||||||
|
validationBehavior: 'live',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(wrapper.vm.context.validationErrors.length).toBe(2);
|
expect(wrapper.vm.context.violations.length).toBe(2);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prevents later error messages when modified rule fails', async () => {
|
it('Prevents later error messages when modified rule fails', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' }
|
propsData: {
|
||||||
|
name: 'test',
|
||||||
|
validation: '^required|in:xyz|min:10,length',
|
||||||
|
validationBehavior: 'live',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(wrapper.vm.context.validationErrors.length).toBe(1);
|
expect(wrapper.vm.context.violations.length).toBe(1);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can bail in the middle of the rule set with a modifier', async () => {
|
it('can bail in the middle of the rule set with a modifier', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' }
|
propsData: {
|
||||||
|
name: 'test',
|
||||||
|
validation: 'required|^in:xyz|min:10,length',
|
||||||
|
validationBehavior: 'live',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(wrapper.vm.context.validationErrors.length).toBe(2);
|
expect(wrapper.vm.context.violations.length).toBe(2);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show errors on blur when set error-behavior is submit', async () => {
|
it('Displays errors when validation-behavior is submit and form is submitted', async () => {
|
||||||
const wrapper = mount(FormularioInput, {
|
|
||||||
propsData: {
|
|
||||||
validation: 'required',
|
|
||||||
errorBehavior: 'submit',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
scopedSlots: {
|
|
||||||
default: `
|
|
||||||
<div>
|
|
||||||
<input v-model="props.context.model" @blur="props.context.blurHandler">
|
|
||||||
<span v-if="props.context.formShouldShowErrors" v-for="error in props.context.allErrors">{{ error.message }}</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
|
||||||
wrapper.find('input').trigger('input')
|
|
||||||
wrapper.find('input').trigger('blur')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays errors when error-behavior is submit and form is submitted', async () => {
|
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { name: 'test' },
|
propsData: { name: 'test' },
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioInput v-slot="inputProps" validation="required" name="testinput" error-behavior="submit">
|
<FormularioInput
|
||||||
<span v-for="error in inputProps.context.allErrors">{{ error.message }}</span>
|
v-slot="{ context }"
|
||||||
|
name="testinput"
|
||||||
|
validation="required"
|
||||||
|
validation-behavior="submit"
|
||||||
|
>
|
||||||
|
<span v-for="error in context.violations">{{ error.message }}</span>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
expect(wrapper.find('span').exists()).toBe(false)
|
||||||
|
|
||||||
wrapper.trigger('submit')
|
wrapper.trigger('submit')
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('span').exists()).toBe(true)
|
expect(wrapper.find('span').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,174 +0,0 @@
|
|||||||
import { cloneDeep, isScalar, parseRules, regexForFormat, snakeToCamel } from '@/libs/utils'
|
|
||||||
import rules from '@/validation/rules.ts'
|
|
||||||
|
|
||||||
describe('parseRules', () => {
|
|
||||||
it('parses single string rules, returning empty arguments array', () => {
|
|
||||||
expect(parseRules('required', rules)).toEqual([
|
|
||||||
[rules.required, [], 'required', null]
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws errors for invalid validation rules', () => {
|
|
||||||
expect(() => {
|
|
||||||
parseRules('required|notarule', rules, null)
|
|
||||||
}).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parses arguments for a rule', () => {
|
|
||||||
expect(parseRules('in:foo,bar', rules)).toEqual([
|
|
||||||
[rules.in, ['foo', 'bar'], 'in', null]
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parses multiple string rules and arguments', () => {
|
|
||||||
expect(parseRules('required|in:foo,bar', rules)).toEqual([
|
|
||||||
[rules.required, [], 'required', null],
|
|
||||||
[rules.in, ['foo', 'bar'], 'in', null]
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parses multiple array rules and arguments', () => {
|
|
||||||
expect(parseRules(['required', 'in:foo,bar'], rules)).toEqual([
|
|
||||||
[rules.required, [], 'required', null],
|
|
||||||
[rules.in, ['foo', 'bar'], 'in', null]
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parses array rules with expression arguments', () => {
|
|
||||||
expect(parseRules([
|
|
||||||
['matches', /^abc/, '1234']
|
|
||||||
], rules)).toEqual([
|
|
||||||
[rules.matches, [/^abc/, '1234'], 'matches', null]
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parses string rules with caret modifier', () => {
|
|
||||||
expect(parseRules('^required|min:10', rules)).toEqual([
|
|
||||||
[rules.required, [], 'required', '^'],
|
|
||||||
[rules.min, ['10'], 'min', null],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('parses array rule with caret modifier', () => {
|
|
||||||
expect(parseRules([['required'], ['^max', '10']], rules)).toEqual([
|
|
||||||
[rules.required, [], 'required', null],
|
|
||||||
[rules.max, ['10'], 'max', '^'],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
describe('regexForFormat', () => {
|
|
||||||
it('allows MM format with other characters', () => expect(regexForFormat('abc/MM').test('abc/01')).toBe(true))
|
|
||||||
|
|
||||||
it('fails MM format with single digit', () => expect(regexForFormat('abc/MM').test('abc/1')).toBe(false))
|
|
||||||
|
|
||||||
it('allows M format with single digit', () => expect(regexForFormat('M/abc').test('1/abc')).toBe(true))
|
|
||||||
|
|
||||||
it('fails MM format when out of range', () => expect(regexForFormat('M/abc').test('13/abc')).toBe(false))
|
|
||||||
|
|
||||||
it('fails M format when out of range', () => expect(regexForFormat('M/abc').test('55/abc')).toBe(false))
|
|
||||||
|
|
||||||
it('Replaces double digits before singles', () => expect(regexForFormat('MMM').test('313131')).toBe(false))
|
|
||||||
|
|
||||||
it('allows DD format with zero digit', () => expect(regexForFormat('xyz/DD').test('xyz/01')).toBe(true))
|
|
||||||
|
|
||||||
it('fails DD format with single digit', () => expect(regexForFormat('xyz/DD').test('xyz/9')).toBe(false))
|
|
||||||
|
|
||||||
it('allows D format with single digit', () => expect(regexForFormat('xyz/D').test('xyz/9')).toBe(true))
|
|
||||||
|
|
||||||
it('fails D format with out of range digit', () => expect(regexForFormat('xyz/D').test('xyz/92')).toBe(false))
|
|
||||||
|
|
||||||
it('fails DD format with out of range digit', () => expect(regexForFormat('xyz/D').test('xyz/32')).toBe(false))
|
|
||||||
|
|
||||||
it('allows YY format with double zeros', () => expect(regexForFormat('YY').test('00')).toBe(true))
|
|
||||||
|
|
||||||
it('fails YY format with four zeros', () => expect(regexForFormat('YY').test('0000')).toBe(false))
|
|
||||||
|
|
||||||
it('allows YYYY format with four zeros', () => expect(regexForFormat('YYYY').test('0000')).toBe(true))
|
|
||||||
|
|
||||||
it('allows MD-YY', () => expect(regexForFormat('MD-YY').test('12-00')).toBe(true))
|
|
||||||
|
|
||||||
it('allows DM-YY', () => expect(regexForFormat('DM-YY').test('12-00')).toBe(true))
|
|
||||||
|
|
||||||
it('allows date like MM/DD/YYYY', () => expect(regexForFormat('MM/DD/YYYY').test('12/18/1987')).toBe(true))
|
|
||||||
|
|
||||||
it('allows date like YYYY-MM-DD', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-31')).toBe(true))
|
|
||||||
|
|
||||||
it('fails date like YYYY-MM-DD with out of bounds day', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false))
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isScalar', () => {
|
|
||||||
it('passes on strings', () => expect(isScalar('hello')).toBe(true))
|
|
||||||
|
|
||||||
it('passes on numbers', () => expect(isScalar(123)).toBe(true))
|
|
||||||
|
|
||||||
it('passes on booleans', () => expect(isScalar(false)).toBe(true))
|
|
||||||
|
|
||||||
it('passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
|
|
||||||
|
|
||||||
it('passes on null', () => expect(isScalar(null)).toBe(true))
|
|
||||||
|
|
||||||
it('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
|
||||||
|
|
||||||
it('fails on pojo', () => expect(isScalar({})).toBe(false))
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('cloneDeep', () => {
|
|
||||||
it('basic objects stay the same', () => expect(cloneDeep({ a: 123, b: 'hello' })).toEqual({ a: 123, b: 'hello' }))
|
|
||||||
|
|
||||||
it('basic nested objects stay the same', () => {
|
|
||||||
expect(cloneDeep({ a: 123, b: { c: 'hello-world' } }))
|
|
||||||
.toEqual({ a: 123, b: { c: 'hello-world' } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('simple pojo reference types are re-created', () => {
|
|
||||||
const c = { c: 'hello-world' }
|
|
||||||
const clone = cloneDeep({ a: 123, b: c })
|
|
||||||
expect(clone.b === c).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('retains array structures inside of a pojo', () => {
|
|
||||||
const obj = { a: 'abcd', d: ['first', 'second'] }
|
|
||||||
const clone = cloneDeep(obj)
|
|
||||||
expect(Array.isArray(clone.d)).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes references inside array structures', () => {
|
|
||||||
const deepObj = {foo: 'bar'}
|
|
||||||
const obj = { a: 'abcd', d: ['first', deepObj] }
|
|
||||||
const clone = cloneDeep(obj)
|
|
||||||
expect(clone.d[1] === deepObj).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('snakeToCamel', () => {
|
|
||||||
it('converts underscore separated words to camelCase', () => {
|
|
||||||
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('converts underscore separated words to camelCase even if they start with a number', () => {
|
|
||||||
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has no effect on already camelCase words', () => {
|
|
||||||
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
|
|
||||||
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ignores double underscores anywhere in a word', () => {
|
|
||||||
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has no effect hyphenated words', () => {
|
|
||||||
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns the same function if passed', () => {
|
|
||||||
const fn = () => {}
|
|
||||||
expect(snakeToCamel(fn)).toBe(fn)
|
|
||||||
})
|
|
||||||
})
|
|
28
test/unit/utils/clone.test.js
Normal file
28
test/unit/utils/clone.test.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import clone from '@/utils/clone'
|
||||||
|
|
||||||
|
describe('clone', () => {
|
||||||
|
it('Basic objects stay the same', () => {
|
||||||
|
const obj = { a: 123, b: 'hello' }
|
||||||
|
expect(clone(obj)).toEqual(obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Basic nested objects stay the same', () => {
|
||||||
|
const obj = { a: 123, b: { c: 'hello-world' } }
|
||||||
|
expect(clone(obj)).toEqual(obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Simple pojo reference types are re-created', () => {
|
||||||
|
const c = { c: 'hello-world' }
|
||||||
|
expect(clone({ a: 123, b: c }).b === c).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Retains array structures inside of a pojo', () => {
|
||||||
|
const obj = { a: 'abcd', d: ['first', 'second'] }
|
||||||
|
expect(Array.isArray(clone(obj).d)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Removes references inside array structures', () => {
|
||||||
|
const obj = { a: 'abcd', d: ['first', { foo: 'bar' }] }
|
||||||
|
expect(clone(obj).d[1] === obj.d[1]).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
17
test/unit/utils/isScalar.test.js
Normal file
17
test/unit/utils/isScalar.test.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import isScalar from '@/utils/isScalar'
|
||||||
|
|
||||||
|
describe('isScalar', () => {
|
||||||
|
it('Passes on strings', () => expect(isScalar('hello')).toBe(true))
|
||||||
|
|
||||||
|
it('Passes on numbers', () => expect(isScalar(123)).toBe(true))
|
||||||
|
|
||||||
|
it('Passes on booleans', () => expect(isScalar(false)).toBe(true))
|
||||||
|
|
||||||
|
it('Passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
|
||||||
|
|
||||||
|
it('Passes on null', () => expect(isScalar(null)).toBe(true))
|
||||||
|
|
||||||
|
it('Passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||||
|
|
||||||
|
it('Fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||||
|
})
|
79
test/unit/utils/regexForFormat.test.js
Normal file
79
test/unit/utils/regexForFormat.test.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import regexForFormat from '@/utils/regexForFormat'
|
||||||
|
|
||||||
|
describe('regexForFormat', () => {
|
||||||
|
it('Allows MM format with other characters', () => {
|
||||||
|
expect(regexForFormat('abc/MM').test('abc/01')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails MM format with single digit', () => {
|
||||||
|
expect(regexForFormat('abc/MM').test('abc/1')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows M format with single digit', () => {
|
||||||
|
expect(regexForFormat('M/abc').test('1/abc')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails MM format when out of range', () => {
|
||||||
|
expect(regexForFormat('M/abc').test('13/abc')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails M format when out of range', () => {
|
||||||
|
expect(regexForFormat('M/abc').test('55/abc')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Replaces double digits before singles', () => {
|
||||||
|
expect(regexForFormat('MMM').test('313131')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows DD format with zero digit', () => {
|
||||||
|
expect(regexForFormat('xyz/DD').test('xyz/01')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails DD format with single digit', () => {
|
||||||
|
expect(regexForFormat('xyz/DD').test('xyz/9')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows D format with single digit', () => {
|
||||||
|
expect(regexForFormat('xyz/D').test('xyz/9')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails D format with out of range digit', () => {
|
||||||
|
expect(regexForFormat('xyz/D').test('xyz/92')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails DD format with out of range digit', () => {
|
||||||
|
expect(regexForFormat('xyz/D').test('xyz/32')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows YY format with double zeros', () => {
|
||||||
|
expect(regexForFormat('YY').test('00')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails YY format with four zeros', () => {
|
||||||
|
expect(regexForFormat('YY').test('0000')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows YYYY format with four zeros', () => {
|
||||||
|
expect(regexForFormat('YYYY').test('0000')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows MD-YY', () => {
|
||||||
|
expect(regexForFormat('MD-YY').test('12-00')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows DM-YY', () => {
|
||||||
|
expect(regexForFormat('DM-YY').test('12-00')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows date like MM/DD/YYYY', () => {
|
||||||
|
expect(regexForFormat('MM/DD/YYYY').test('12/18/1987')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows date like YYYY-MM-DD', () => {
|
||||||
|
expect(regexForFormat('YYYY-MM-DD').test('1987-01-31')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fails date like YYYY-MM-DD with out of bounds day', () => {
|
||||||
|
expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
27
test/unit/utils/snakeToCamel.test.js
Normal file
27
test/unit/utils/snakeToCamel.test.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import snakeToCamel from '@/utils/snakeToCamel'
|
||||||
|
|
||||||
|
describe('snakeToCamel', () => {
|
||||||
|
it('Converts underscore separated words to camelCase', () => {
|
||||||
|
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Converts underscore separated words to camelCase even if they start with a number', () => {
|
||||||
|
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Has no effect on already camelCase words', () => {
|
||||||
|
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
|
||||||
|
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Ignores double underscores anywhere in a word', () => {
|
||||||
|
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Has no effect hyphenated words', () => {
|
||||||
|
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
|
||||||
|
})
|
||||||
|
})
|
@ -1,6 +1,5 @@
|
|||||||
import rules from '@/validation/rules.ts'
|
import rules from '@/validation/rules.ts'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepted rule
|
* Accepted rule
|
||||||
*/
|
*/
|
||||||
@ -143,29 +142,29 @@ describe('between', () => {
|
|||||||
* Confirm
|
* Confirm
|
||||||
*/
|
*/
|
||||||
describe('confirm', () => {
|
describe('confirm', () => {
|
||||||
it('passes when the values are the same strings', async () => expect(await rules.confirm(
|
it('Passes when the values are the same strings', async () => expect(await rules.confirm(
|
||||||
{ value: 'abc', name: 'password', getFormValues: () => ({ password_confirm: 'abc' }) }
|
{ value: 'abc', name: 'password', formValues: { password_confirm: 'abc' } }
|
||||||
)).toBe(true))
|
)).toBe(true))
|
||||||
|
|
||||||
it('passes when the values are the same integers', async () => expect(await rules.confirm(
|
it('Passes when the values are the same integers', async () => expect(await rules.confirm(
|
||||||
{ value: 4422132, name: 'xyz', getFormValues: () => ({ xyz_confirm: 4422132 }) }
|
{ value: 4422132, name: 'xyz', formValues: { xyz_confirm: 4422132 } }
|
||||||
)).toBe(true))
|
)).toBe(true))
|
||||||
|
|
||||||
it('passes when using a custom field', async () => expect(await rules.confirm(
|
it('Passes when using a custom field', async () => expect(await rules.confirm(
|
||||||
{ value: 4422132, name: 'name', getFormValues: () => ({ other_field: 4422132 }) },
|
{ value: 4422132, name: 'name', formValues: { other_field: 4422132 } },
|
||||||
'other_field'
|
'other_field'
|
||||||
)).toBe(true))
|
)).toBe(true))
|
||||||
|
|
||||||
it('passes when using a field ends in _confirm', async () => expect(await rules.confirm(
|
it('Passes when using a field ends in _confirm', async () => expect(await rules.confirm(
|
||||||
{ value: '$ecret', name: 'password_confirm', getFormValues: () => ({ password: '$ecret' }) }
|
{ value: '$ecret', name: 'password_confirm', formValues: { password: '$ecret' } }
|
||||||
)).toBe(true))
|
)).toBe(true))
|
||||||
|
|
||||||
it('fails when using different strings', async () => expect(await rules.confirm(
|
it('Fails when using different strings', async () => expect(await rules.confirm(
|
||||||
{ value: 'Justin', name: 'name', getFormValues: () => ({ name_confirm: 'Daniel' }) },
|
{ value: 'Justin', name: 'name', formValues: { name_confirm: 'Daniel' } },
|
||||||
)).toBe(false))
|
)).toBe(false))
|
||||||
|
|
||||||
it('fails when the types are different', async () => expect(await rules.confirm(
|
it('Fails when the types are different', async () => expect(await rules.confirm(
|
||||||
{ value: '1234', name: 'num', getFormValues: () => ({ num_confirm: 1234 }) },
|
{ value: '1234', name: 'num', formValues: { num_confirm: 1234 } },
|
||||||
)).toBe(false))
|
)).toBe(false))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,36 +1,223 @@
|
|||||||
import { enlarge } from '@/validation/validator.ts'
|
import {
|
||||||
|
createValidator,
|
||||||
|
enlarge,
|
||||||
|
parseModifier,
|
||||||
|
processSingleArrayConstraint,
|
||||||
|
processSingleStringConstraint,
|
||||||
|
validate,
|
||||||
|
} from '@/validation/validator.ts'
|
||||||
|
|
||||||
// @TODO: Converting raw rule data to validator
|
const isNumberAndInRangeRule = ({ value }, from, to) => !isNaN(value) && value >= from && value <= to
|
||||||
|
const isNumberAndInRangeMessage = ({ value }, from, to) => {
|
||||||
|
return isNaN(value) ? 'Value is NaN' : `Value not in range [${from}, ${to}]`
|
||||||
|
}
|
||||||
|
|
||||||
describe('Validator', () => {
|
describe('createValidator', () => {
|
||||||
it ('Enlarges validator groups', () => {
|
it ('Creates correct validator', async () => {
|
||||||
expect(enlarge([{
|
const context = { value: 'abc', formValues: {}, name: 'field' }
|
||||||
validators: [],
|
const validate = createValidator(
|
||||||
bail: false,
|
isNumberAndInRangeRule,
|
||||||
}, {
|
'rule',
|
||||||
validators: [],
|
[1, 2],
|
||||||
bail: false,
|
isNumberAndInRangeMessage,
|
||||||
}, {
|
)
|
||||||
validators: [],
|
|
||||||
bail: false,
|
await expect(validate(context)).toBeInstanceOf(Promise)
|
||||||
}, {
|
expect(await validate(context)).toEqual({
|
||||||
validators: [],
|
rule: 'rule',
|
||||||
bail: true,
|
args: [1, 2],
|
||||||
}, {
|
context,
|
||||||
validators: [],
|
message: 'Value is NaN',
|
||||||
bail: false,
|
})
|
||||||
}, {
|
|
||||||
validators: [],
|
expect(await validate({ ...context, value: 0 })).toEqual({
|
||||||
bail: false,
|
rule: 'rule',
|
||||||
}])).toEqual([{
|
args: [1, 2],
|
||||||
validators: [],
|
context: { ...context, value: 0 },
|
||||||
bail: false,
|
message: 'Value not in range [1, 2]',
|
||||||
}, {
|
})
|
||||||
validators: [],
|
|
||||||
bail: true,
|
expect(await validate({ ...context, value: 1.5 })).toBeNull()
|
||||||
}, {
|
})
|
||||||
validators: [],
|
})
|
||||||
bail: false,
|
|
||||||
}])
|
describe('enlarge', () => {
|
||||||
|
it ('Merges non-bail validator groups', () => {
|
||||||
|
expect(enlarge([
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
])).toEqual([
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('Merges non-bail validator groups, bail groups stayed unmerged', () => {
|
||||||
|
expect(enlarge([
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: true },
|
||||||
|
{ validators: [], bail: true },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
])).toEqual([
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
{ validators: [], bail: true },
|
||||||
|
{ validators: [], bail: true },
|
||||||
|
{ validators: [], bail: false },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseModifier', () => {
|
||||||
|
it ('Extracts modifier if present', () => {
|
||||||
|
expect(parseModifier('^required')).toEqual(['required', '^'])
|
||||||
|
expect(parseModifier('required')).toEqual(['required', null])
|
||||||
|
expect(parseModifier('bail')).toEqual(['bail', null])
|
||||||
|
expect(parseModifier('^min_length')).toEqual(['minLength', '^'])
|
||||||
|
expect(parseModifier('min_length')).toEqual(['minLength', null])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('processSingleArrayConstraint', () => {
|
||||||
|
const rules = { isNumberAndInRange: isNumberAndInRangeRule }
|
||||||
|
const messages = { isNumberAndInRange: isNumberAndInRangeMessage }
|
||||||
|
|
||||||
|
it ('Creates validator context if constraint is valid and rule exists', () => {
|
||||||
|
expect(processSingleArrayConstraint(['isNumberAndInRange', 1, 2], rules, messages)).toEqual([
|
||||||
|
expect.any(Function),
|
||||||
|
'isNumberAndInRange',
|
||||||
|
null,
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(processSingleArrayConstraint(['^is_number_and_in_range', 1, 2], rules, messages)).toEqual([
|
||||||
|
expect.any(Function),
|
||||||
|
'isNumberAndInRange',
|
||||||
|
'^',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('Creates validator context if constraint is validator', () => {
|
||||||
|
const validate = createValidator(
|
||||||
|
isNumberAndInRangeRule,
|
||||||
|
null,
|
||||||
|
[],
|
||||||
|
isNumberAndInRangeMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(processSingleArrayConstraint([validate], rules, messages)).toEqual([
|
||||||
|
expect.any(Function),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('Throws error if constraint is valid and rule not exists', () => {
|
||||||
|
expect(() => processSingleArrayConstraint(
|
||||||
|
['^rule_that_not_exists'],
|
||||||
|
{ rule: isNumberAndInRangeRule },
|
||||||
|
{ rule: isNumberAndInRangeMessage },
|
||||||
|
)).toThrow('[Formulario] Can\'t create validator for constraint: [\"^rule_that_not_exists\"]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('Throws error if constraint is not valid', () => {
|
||||||
|
expect(() => processSingleArrayConstraint(
|
||||||
|
[null],
|
||||||
|
{ rule: isNumberAndInRangeRule },
|
||||||
|
{ rule: isNumberAndInRangeMessage },
|
||||||
|
)).toThrow('[Formulario]: For array constraint first element must be rule name or Validator function')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('processSingleStringConstraint', () => {
|
||||||
|
const rules = { isNumberAndInRange: isNumberAndInRangeRule }
|
||||||
|
const messages = { isNumberAndInRange: isNumberAndInRangeMessage }
|
||||||
|
|
||||||
|
it ('Creates validator context if constraint is valid and rule exists', () => {
|
||||||
|
expect(processSingleStringConstraint('isNumberAndInRange:1,2', rules, messages)).toEqual([
|
||||||
|
expect.any(Function),
|
||||||
|
'isNumberAndInRange',
|
||||||
|
null,
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(processSingleStringConstraint('^is_number_and_in_range:1,2', rules, messages)).toEqual([
|
||||||
|
expect.any(Function),
|
||||||
|
'isNumberAndInRange',
|
||||||
|
'^',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('Throws error if constraint is valid and rule not exists', () => {
|
||||||
|
expect(() => processSingleStringConstraint(
|
||||||
|
'^rule_that_not_exists',
|
||||||
|
{ rule: isNumberAndInRangeRule },
|
||||||
|
{ rule: isNumberAndInRangeMessage },
|
||||||
|
)).toThrow('[Formulario] Can\'t create validator for constraint: ^rule_that_not_exists')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validate', () => {
|
||||||
|
const isNumber = createValidator(
|
||||||
|
({ value }) => String(value) !== '' && !isNaN(value),
|
||||||
|
'number',
|
||||||
|
[],
|
||||||
|
() => 'Value is NaN'
|
||||||
|
)
|
||||||
|
const isRequired = createValidator(
|
||||||
|
({ value }) => value !== undefined && String(value) !== '',
|
||||||
|
'required',
|
||||||
|
[],
|
||||||
|
() => 'Value is required'
|
||||||
|
)
|
||||||
|
const context = { value: '', formValues: {}, name: 'field' }
|
||||||
|
|
||||||
|
it('Applies all rules if no bail', async () => {
|
||||||
|
expect(await validate([
|
||||||
|
[isRequired, 'required', null],
|
||||||
|
[isNumber, 'number', null],
|
||||||
|
], context)).toEqual([{
|
||||||
|
rule: 'required',
|
||||||
|
args: [],
|
||||||
|
context,
|
||||||
|
message: 'Value is required',
|
||||||
|
}, {
|
||||||
|
rule: 'number',
|
||||||
|
args: [],
|
||||||
|
context,
|
||||||
|
message: 'Value is NaN',
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Applies only first rule (bail)', async () => {
|
||||||
|
expect(await validate([
|
||||||
|
[() => {}, 'bail', null],
|
||||||
|
[isRequired, 'required', '^'],
|
||||||
|
[isNumber, 'number', null],
|
||||||
|
], context)).toEqual([{
|
||||||
|
rule: 'required',
|
||||||
|
args: [],
|
||||||
|
context,
|
||||||
|
message: 'Value is required',
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Applies only first rule (bail modifier)', async () => {
|
||||||
|
expect(await validate([
|
||||||
|
[isRequired, 'required', '^'],
|
||||||
|
[isNumber, 'number', null],
|
||||||
|
], context)).toEqual([{
|
||||||
|
rule: 'required',
|
||||||
|
args: [],
|
||||||
|
context,
|
||||||
|
message: 'Value is required',
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('No violations on valid context', async () => {
|
||||||
|
expect(await validate([
|
||||||
|
[isRequired, 'required', '^'],
|
||||||
|
[isNumber, 'number', null],
|
||||||
|
], { ...context, value: 0 })).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user