diff --git a/README.md b/README.md
index ea4f6b3..ba3625c 100644
--- a/README.md
+++ b/README.md
@@ -1,86 +1,90 @@
## What is Vue Formulario?
-Vue Formulario is a library, based on Vue Formulate, that handles the core logic for working with forms and gives full control on the form presentation.
+Vue Formulario is a library, based on Vue Formulate, that handles the core logic
+for working with forms and gives full control on the form presentation.
## Examples
-Every form control have to rendered inside FormularioInput component. This component provides `id` and `context` in v-slot props. Control should use `context.model` as v-model and `context.blurHandler` as handler for `blur` event (it is necessary for validation when property `errorBehavior` is `blur`). Errors object list for field can be accessed through `context.allErrors`. Each error is an object with fields message (translated message), rule (rule name) and context.
+Every form control have to rendered inside FormularioInput component. This component provides `id` and `context` in
+v-slot props. Control should use `context.model` as v-model and `context.runValidation` as handler for `blur` event
+(it is necessary for validation when property `validationBehavior` is `demand`). Errors list for a field can be
+accessed through `context.allErrors`.
The example below creates the authorization form from data:
```json
- {
- "username": "",
- "password": "",
- "options": {
- "anonym": false,
- "tags": ["test"]
- },
+{
+ "username": "",
+ "password": "",
+ "options": {
+ "anonymous": false,
+ "tags": ["test"]
}
+}
```
```html
-
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
- {{ error.message }}
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
```
## License
diff --git a/src/Formulario.ts b/src/Formulario.ts
index 90f046e..9c41268 100644
--- a/src/Formulario.ts
+++ b/src/Formulario.ts
@@ -1,45 +1,29 @@
-import { VueConstructor } from 'vue'
-
import merge from '@/utils/merge'
import validationRules from '@/validation/rules'
import validationMessages from '@/validation/messages'
-import FormularioForm from '@/FormularioForm.vue'
-import FormularioGrouping from '@/FormularioGrouping.vue'
-import FormularioInput from '@/FormularioInput.vue'
-
import {
ValidationContext,
- CheckRuleFn,
- CreateMessageFn,
+ ValidationRuleFn,
+ ValidationMessageFn,
+ ValidationMessageI18NFn,
} from '@/validation/validator'
-interface FormularioOptions {
- validationRules?: any;
- validationMessages?: Record;
+export interface FormularioOptions {
+ validationRules?: Record;
+ validationMessages?: Record;
}
-// noinspection JSUnusedGlobalSymbols
/**
* The base formulario library.
*/
export default class Formulario {
- public validationRules: Record = {}
- public validationMessages: Record = {}
+ public validationRules: Record = {}
+ public validationMessages: Record = {}
- constructor () {
+ constructor (options?: FormularioOptions) {
this.validationRules = validationRules
this.validationMessages = validationMessages
- }
-
- /**
- * Install vue formulario, and register it’s components.
- */
- install (Vue: VueConstructor, options?: FormularioOptions): void {
- Vue.prototype.$formulario = this
- Vue.component('FormularioForm', FormularioForm)
- Vue.component('FormularioGrouping', FormularioGrouping)
- Vue.component('FormularioInput', FormularioInput)
this.extend(options || {})
}
@@ -59,16 +43,16 @@ export default class Formulario {
/**
* Get validation rules by merging any passed in with global rules.
*/
- getRules (extendWith: Record = {}): Record {
+ getRules (extendWith: Record = {}): Record {
return merge(this.validationRules, extendWith)
}
/**
* Get validation messages by merging any passed in with global messages.
*/
- getMessages (vm: Vue, extendWith: Record): Record {
+ getMessages (vm: Vue, extendWith: Record): Record {
const raw = merge(this.validationMessages || {}, extendWith)
- const messages: Record = {}
+ const messages: Record = {}
for (const name in raw) {
messages[name] = (context: ValidationContext, ...args: any[]): string => {
diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue
index 3935375..4dfc006 100644
--- a/src/FormularioForm.vue
+++ b/src/FormularioForm.vue
@@ -126,7 +126,7 @@ export default class FormularioForm extends Vue {
@Provide('formularioRegister')
register (field: string, component: FormularioInput): void {
- this.registry.register(field, component)
+ this.registry.add(field, component)
}
@Provide('formularioDeregister')
@@ -154,7 +154,7 @@ export default class FormularioForm extends Vue {
const newValue = getNested(values, registryKey)
if (!shallowEqualObjects(newValue, oldValue)) {
- this.setFieldValue(registryKey, newValue, false)
+ this.setFieldValue(registryKey, newValue)
proxyHasChanges = true
}
@@ -171,8 +171,7 @@ export default class FormularioForm extends Vue {
}
}
- @Provide('formularioSetter')
- setFieldValue (field: string, value: any, emit = true): void {
+ setFieldValue (field: string, value: any): void {
if (value === undefined) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [field]: value, ...proxy } = this.proxy
@@ -180,10 +179,12 @@ export default class FormularioForm extends Vue {
} else {
setNested(this.proxy, field, value)
}
+ }
- if (emit) {
- this.$emit('input', Object.assign({}, this.proxy))
- }
+ @Provide('formularioSetter')
+ setFieldValueAndEmit (field: string, value: any): void {
+ this.setFieldValue(field, value)
+ this.$emit('input', { ...this.proxy })
}
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record }): void {
diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue
index c2247a3..1b993b3 100644
--- a/src/FormularioInput.vue
+++ b/src/FormularioInput.vue
@@ -15,8 +15,8 @@ import {
} from 'vue-property-decorator'
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './utils'
import {
- CheckRuleFn,
- CreateMessageFn,
+ ValidationRuleFn,
+ ValidationMessageI18NFn,
processConstraints,
validate,
Violation,
@@ -47,8 +47,8 @@ export default class FormularioInput extends Vue {
}) name!: string
@Prop({ default: '' }) validation!: string|any[]
- @Prop({ default: () => ({}) }) validationRules!: Record
- @Prop({ default: () => ({}) }) validationMessages!: Record
+ @Prop({ default: () => ({}) }) validationRules!: Record
+ @Prop({ default: () => ({}) }) validationMessages!: Record
@Prop({
default: VALIDATION_BEHAVIOR.DEMAND,
validator: behavior => Object.values(VALIDATION_BEHAVIOR).includes(behavior)
@@ -69,10 +69,7 @@ export default class FormularioInput extends Vue {
get model (): any {
const model = this.hasModel ? 'value' : 'proxy'
- if (this[model] === undefined) {
- return ''
- }
- return this[model]
+ return this[model] !== undefined ? this[model] : ''
}
set model (value: any) {
@@ -102,17 +99,17 @@ export default class FormularioInput extends Vue {
})
}
- get normalizedValidationRules (): Record {
- const rules: Record = {}
+ get normalizedValidationRules (): Record {
+ const rules: Record = {}
Object.keys(this.validationRules).forEach(key => {
rules[snakeToCamel(key)] = this.validationRules[key]
})
return rules
}
- get normalizedValidationMessages (): Record {
- const messages: Record = {}
- Object.keys(this.validationMessages).forEach((key) => {
+ get normalizedValidationMessages (): Record {
+ const messages: Record = {}
+ Object.keys(this.validationMessages).forEach(key => {
messages[snakeToCamel(key)] = this.validationMessages[key]
})
return messages
diff --git a/src/form/registry.ts b/src/form/registry.ts
index 8319922..07e2ae7 100644
--- a/src/form/registry.ts
+++ b/src/form/registry.ts
@@ -20,10 +20,34 @@ export default class Registry {
}
/**
- * Add an item to the registry.
+ * Fully register a component.
+ * @param {string} field name of the field.
+ * @param {FormularioForm} component the actual component instance.
*/
- add (name: string, component: FormularioInput): void {
- this.registry.set(name, component)
+ add (field: string, component: FormularioInput): void {
+ if (this.registry.has(field)) {
+ return
+ }
+
+ this.registry.set(field, component)
+
+ // @ts-ignore
+ const value = getNested(this.ctx.initialValues, field)
+ const hasModel = has(component.$options.propsData || {}, 'value')
+
+ // @ts-ignore
+ if (!hasModel && this.ctx.hasInitialValue && value !== undefined) {
+ // In the case that the form is carrying an initial value and the
+ // element is not, set it directly.
+ // @ts-ignore
+ component.context.model = value
+ // @ts-ignore
+ } else if (hasModel && !shallowEqualObjects(component.proxy, value)) {
+ // In this case, the field is v-modeled or has an initial value and the
+ // form has no value or a different value, so use the field value
+ // @ts-ignore
+ this.ctx.setFieldValueAndEmit(field, component.proxy)
+ }
}
/**
@@ -103,40 +127,6 @@ export default class Registry {
return Array.from(this.registry.keys())
}
- /**
- * Fully register a component.
- * @param {string} field name of the field.
- * @param {FormularioForm} component the actual component instance.
- */
- register (field: string, component: FormularioInput): void {
- if (this.registry.has(field)) {
- return
- }
- this.registry.set(field, component)
- const hasModel = has(component.$options.propsData || {}, 'value')
- if (
- !hasModel &&
- // @ts-ignore
- this.ctx.hasInitialValue &&
- // @ts-ignore
- getNested(this.ctx.initialValues, field) !== undefined
- ) {
- // In the case that the form is carrying an initial value and the
- // element is not, set it directly.
- // @ts-ignore
- component.context.model = getNested(this.ctx.initialValues, field)
- } else if (
- hasModel &&
- // @ts-ignore
- !shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
- ) {
- // In this case, the field is v-modeled or has an initial value and the
- // form has no value or a different value, so use the field value
- // @ts-ignore
- this.ctx.setFieldValue(field, component.proxy)
- }
- }
-
/**
* Reduce the registry.
* @param {function} callback
diff --git a/src/index.ts b/src/index.ts
index 5bf2957..a8e34b8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,27 @@
-import Formulario from '@/Formulario.ts'
+import Formulario, { FormularioOptions } from '@/Formulario.ts'
+import { VueConstructor } from 'vue'
+import FormularioForm from '@/FormularioForm.vue'
+import FormularioGrouping from '@/FormularioGrouping.vue'
+import FormularioInput from '@/FormularioInput.vue'
-export default new Formulario()
+export default {
+ install (Vue: VueConstructor, options?: FormularioOptions): void {
+ Vue.component('FormularioForm', FormularioForm)
+ Vue.component('FormularioGrouping', FormularioGrouping)
+ Vue.component('FormularioInput', FormularioInput)
+
+ Vue.mixin({
+ beforeCreate () {
+ const o = this.$options as Record
+
+ if (typeof o.formulario === 'function') {
+ this.$formulario = o.formulario()
+ } else if (o.parent && o.parent.$formulario) {
+ this.$formulario = o.parent.$formulario
+ } else {
+ this.$formulario = new Formulario(options)
+ }
+ }
+ })
+ },
+}
diff --git a/src/utils/clone.ts b/src/utils/clone.ts
index 0492ace..acda4ec 100644
--- a/src/utils/clone.ts
+++ b/src/utils/clone.ts
@@ -14,7 +14,13 @@ export default function clone (value: any): any {
for (const key in value) {
if (has(value, key)) {
- copy[key] = isScalar(value[key]) ? value[key] : clone(value[key])
+ if (isScalar(value[key])) {
+ copy[key] = value[key]
+ } else if (value instanceof Date) {
+ copy[key] = new Date(copy[key])
+ } else {
+ copy[key] = clone(value[key])
+ }
}
}
diff --git a/src/validation/messages.ts b/src/validation/messages.ts
index 902aea1..90bae34 100644
--- a/src/validation/messages.ts
+++ b/src/validation/messages.ts
@@ -1,23 +1,25 @@
-import { ValidationContext } from '@/validation/validator'
+import {
+ ValidationContext,
+ ValidationMessageI18NFn,
+} from '@/validation/validator'
-export default {
+/**
+ * Message builders, names match rules names, see @/validation/rules
+ */
+const messages: Record = {
/**
- * The default render method for error messages.
+ * Fallback for rules without message builder
+ * @param vm
+ * @param context
*/
default (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.default', context)
},
- /**
- * Valid accepted value.
- */
accepted (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.accepted', context)
},
- /**
- * The date is not after.
- */
after (vm: Vue, context: ValidationContext, compare: string | false = false): string {
if (typeof compare === 'string' && compare.length) {
return vm.$t('validation.after.compare', context)
@@ -26,23 +28,14 @@ export default {
return vm.$t('validation.after.default', context)
},
- /**
- * The value is not a letter.
- */
- alpha (vm: Vue, context: Record): string {
+ alpha (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.alpha', context)
},
- /**
- * Rule: checks if the value is alpha numeric
- */
alphanumeric (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.alphanumeric', context)
},
- /**
- * The date is not before.
- */
before (vm: Vue, context: ValidationContext, compare: string|false = false): string {
if (typeof compare === 'string' && compare.length) {
return vm.$t('validation.before.compare', context)
@@ -51,9 +44,6 @@ export default {
return vm.$t('validation.before.default', context)
},
- /**
- * The value is not between two numbers or lengths
- */
between (vm: Vue, context: ValidationContext, from: number|any = 0, to: number|any = 10, force?: string): string {
const data = { ...context, from, to }
@@ -64,16 +54,10 @@ export default {
return vm.$t('validation.between.default', data)
},
- /**
- * The confirmation field does not match
- */
confirm (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.confirm', context)
},
- /**
- * Is not a valid date.
- */
date (vm: Vue, context: ValidationContext, format: string | false = false): string {
if (typeof format === 'string' && format.length) {
return vm.$t('validation.date.format', context)
@@ -82,24 +66,15 @@ export default {
return vm.$t('validation.date.default', context)
},
- /**
- * Is not a valid email address.
- */
email (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.email.default', context)
},
- /**
- * Ends with specified value
- */
endsWith (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.endsWith.default', context)
},
- /**
- * Value is an allowed value.
- */
- in: function (vm: Vue, context: ValidationContext): string {
+ in (vm: Vue, context: ValidationContext): string {
if (typeof context.value === 'string' && context.value) {
return vm.$t('validation.in.string', context)
}
@@ -107,16 +82,10 @@ export default {
return vm.$t('validation.in.default', context)
},
- /**
- * Value is not a match.
- */
matches (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.matches.default', context)
},
- /**
- * The maximum value allowed.
- */
max (vm: Vue, context: ValidationContext, maximum: string | number = 10, force?: string): string {
if (Array.isArray(context.value)) {
return vm.$tc('validation.max.array', maximum, context)
@@ -129,9 +98,6 @@ export default {
return vm.$tc('validation.max.default', maximum, context)
},
- /**
- * The maximum value allowed.
- */
min (vm: Vue, context: ValidationContext, minimum: number | any = 1, force?: string): string {
if (Array.isArray(context.value)) {
return vm.$tc('validation.min.array', minimum, context)
@@ -144,38 +110,25 @@ export default {
return vm.$tc('validation.min.default', minimum, context)
},
- /**
- * The field is not an allowed value
- */
not (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.not.default', context)
},
- /**
- * The field is not a number
- */
number (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.number.default', context)
},
- /**
- * Required field.
- */
required (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.required.default', context)
},
- /**
- * Starts with specified value
- */
startsWith (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.startsWith.default', context)
},
- /**
- * Value is not a url.
- */
- url (vm: Vue, context: Record): string {
+ url (vm: Vue, context: ValidationContext): string {
return vm.$t('validation.url.default', context)
}
}
+
+export default messages
diff --git a/src/validation/rules.ts b/src/validation/rules.ts
index 12c0820..c88f86c 100644
--- a/src/validation/rules.ts
+++ b/src/validation/rules.ts
@@ -1,264 +1,264 @@
import isUrl from 'is-url'
-import { shallowEqualObjects, regexForFormat, has } from '@/utils'
-import { ValidationContext } from '@/validation/validator'
+import { has, regexForFormat, shallowEqualObjects } from '@/utils'
+import {
+ ValidationContext,
+ ValidationRuleFn,
+} from '@/validation/validator'
-interface DateValidationContext extends ValidationContext {
- value: Date|string;
-}
-
-export default {
+const rules: Record = {
/**
* Rule: the value must be "yes", "on", "1", or true
*/
- accepted ({ value }: ValidationContext): Promise {
- return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
+ accepted ({ value }: ValidationContext): boolean {
+ return ['yes', 'on', '1', 1, true, 'true'].includes(value)
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
- after ({ value }: DateValidationContext, compare: string | false = false): Promise {
- const timestamp = compare !== false ? Date.parse(compare) : Date.now()
- const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
- return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
+ after ({ value }: ValidationContext, compare: string | false = false): boolean {
+ const compareTimestamp = compare !== false ? Date.parse(compare) : Date.now()
+ const valueTimestamp = value instanceof Date ? value.getTime() : Date.parse(value)
+
+ return isNaN(valueTimestamp) ? false : (valueTimestamp > compareTimestamp)
},
/**
* Rule: checks if the value is only alpha
*/
- alpha ({ value }: { value: string }, set = 'default'): Promise {
+ alpha ({ value }: ValidationContext, set = 'default'): boolean {
const sets: Record = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
- latin: /^[a-zA-Z]+$/
+ latin: /^[a-zA-Z]+$/,
}
- const selectedSet = has(sets, set) ? set : 'default'
- return Promise.resolve(sets[selectedSet].test(value))
+ return typeof value === 'string' && sets[has(sets, set) ? set : 'default'].test(value)
},
/**
* Rule: checks if the value is alpha numeric
*/
- alphanumeric ({ value }: { value: string }, set = 'default'): Promise {
+ alphanumeric ({ value }: ValidationContext, set = 'default'): boolean {
const sets: Record = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]+$/
}
- const selectedSet = has(sets, set) ? set : 'default'
- return Promise.resolve(sets[selectedSet].test(value))
+ return typeof value === 'string' && sets[has(sets, set) ? set : 'default'].test(value)
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
- before ({ value }: DateValidationContext, compare: string|false = false): Promise {
- const timestamp = compare !== false ? Date.parse(compare) : Date.now()
- const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
- return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
+ before ({ value }: ValidationContext, compare: string|false = false): boolean {
+ const compareTimestamp = compare !== false ? Date.parse(compare) : Date.now()
+ const valueTimestamp = value instanceof Date ? value.getTime() : Date.parse(value)
+
+ return isNaN(valueTimestamp) ? false : (valueTimestamp < compareTimestamp)
},
/**
* Rule: checks if the value is between two other values
*/
- between ({ value }: { value: string|number }, from: number|any = 0, to: number|any = 10, force?: string): Promise {
- return Promise.resolve(((): boolean => {
- if (from === null || to === null || isNaN(from) || isNaN(to)) {
- return false
- }
- if ((!isNaN(Number(value)) && force !== 'length') || force === 'value') {
- value = Number(value)
- from = Number(from)
- to = Number(to)
- return (value > from && value < to)
- }
- if (typeof value === 'string' || force === 'length') {
- value = (!isNaN(Number(value)) ? value.toString() : value) as string
- return value.length > from && value.length < to
- }
+ between ({ value }: ValidationContext, from: number|any = 0, to: number|any = 10, force?: string): boolean {
+ if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false
- })())
+ }
+
+ if ((!isNaN(Number(value)) && force !== 'length') || force === 'value') {
+ value = Number(value)
+ return (value > Number(from) && value < Number(to))
+ }
+
+ if (typeof value === 'string' || force === 'length') {
+ value = (!isNaN(Number(value)) ? value.toString() : value) as string
+ return value.length > from && value.length < to
+ }
+
+ return false
},
/**
* Confirm that the value of one field is the same as another, mostly used
* for password confirmations.
*/
- confirm ({ value, formValues, name }: ValidationContext, field?: string): Promise {
- return Promise.resolve(((): boolean => {
- let confirmationFieldName = field
- if (!confirmationFieldName) {
- confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
- }
- return formValues[confirmationFieldName] === value
- })())
+ confirm ({ value, formValues, name }: ValidationContext, field?: string): boolean {
+ let confirmationFieldName = field
+ if (!confirmationFieldName) {
+ confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
+ }
+ return formValues[confirmationFieldName] === value
},
/**
* Rule: ensures the value is a date according to Date.parse(), or a format
* regex.
*/
- date ({ value }: { value: string }, format: string | false = false): Promise {
- return Promise.resolve(format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value)))
+ date ({ value }: ValidationContext, format: string | false = false): boolean {
+ return format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value))
},
/**
* Rule: tests
*/
- email ({ value }: { value: string }): Promise {
+ email ({ value }: ValidationContext): boolean {
if (!value) {
- return Promise.resolve(true)
+ return true
}
// eslint-disable-next-line
const isEmail = /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
- return Promise.resolve(isEmail.test(value))
+ return isEmail.test(value)
},
/**
* Rule: Value ends with one of the given Strings
*/
- endsWith ({ value }: { value: any }, ...stack: any[]): Promise {
+ endsWith ({ value }: ValidationContext, ...stack: any[]): boolean {
if (!value) {
- return Promise.resolve(true)
+ return true
}
if (typeof value === 'string') {
- return Promise.resolve(stack.length === 0 || stack.some(str => value.endsWith(str)))
+ return stack.length === 0 || stack.some(str => value.endsWith(str))
}
- return Promise.resolve(false)
+ return false
},
/**
* Rule: Value is in an array (stack).
*/
- in ({ value }: { value: any }, ...stack: any[]): Promise {
- return Promise.resolve(stack.some(item => {
- return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value
- }))
+ in ({ value }: ValidationContext, ...stack: any[]): boolean {
+ return stack.some(item => typeof item === 'object' ? shallowEqualObjects(item, value) : item === value)
},
/**
* Rule: Match the value against a (stack) of patterns or strings
*/
- matches ({ value }: { value: any }, ...stack: any[]): Promise {
- return Promise.resolve(!!stack.find(pattern => {
+ matches ({ value }: ValidationContext, ...stack: any[]): boolean {
+ return !!stack.find(pattern => {
if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') {
pattern = new RegExp(pattern.substr(1, pattern.length - 2))
}
+
if (pattern instanceof RegExp) {
return pattern.test(value)
}
+
return pattern === value
- }))
+ })
},
/**
* Check the maximum value of a particular.
*/
- max ({ value }: { value: any }, maximum: string | number = 10, force?: string): Promise {
- return Promise.resolve(((): boolean => {
- if (Array.isArray(value)) {
- maximum = !isNaN(Number(maximum)) ? Number(maximum) : maximum
- return value.length <= maximum
- }
- if ((!isNaN(value) && force !== 'length') || force === 'value') {
- value = !isNaN(value) ? Number(value) : value
- return value <= maximum
- }
- if (typeof value === 'string' || (force === 'length')) {
- value = !isNaN(value) ? value.toString() : value
- return value.length <= maximum
- }
- return false
- })())
+ max ({ value }: ValidationContext, maximum: string | number = 10, force?: string): boolean {
+ if (Array.isArray(value)) {
+ maximum = !isNaN(Number(maximum)) ? Number(maximum) : maximum
+ return value.length <= maximum
+ }
+
+ if ((!isNaN(value) && force !== 'length') || force === 'value') {
+ value = !isNaN(value) ? Number(value) : value
+ return value <= maximum
+ }
+
+ if (typeof value === 'string' || (force === 'length')) {
+ value = !isNaN(value) ? value.toString() : value
+ return value.length <= maximum
+ }
+
+ return false
},
/**
* Check the minimum value of a particular.
*/
- min ({ value }: { value: any }, minimum: number | any = 1, force?: string): Promise {
- 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
- })())
+ min ({ value }: ValidationContext, minimum: number | any = 1, force?: string): 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.
*/
- not ({ value }: { value: any }, ...stack: any[]): Promise {
- return Promise.resolve(!stack.some(item => {
- return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value
- }))
+ not ({ value }: ValidationContext, ...stack: any[]): boolean {
+ return !stack.some(item => typeof item === 'object' ? shallowEqualObjects(item, value) : item === value)
},
/**
* Rule: checks if the value is only alpha numeric
*/
- number ({ value }: { value: any }): Promise {
- return Promise.resolve(String(value).length > 0 && !isNaN(Number(value)))
+ number ({ value }: ValidationContext): boolean {
+ return String(value).length > 0 && !isNaN(Number(value))
},
/**
* Rule: must be a value
*/
- required ({ value }: { value: any }, isRequired: string|boolean = true): Promise {
- return Promise.resolve(((): boolean => {
- if (!isRequired || ['no', 'false'].includes(isRequired as string)) {
- return true
- }
- if (Array.isArray(value)) {
- return !!value.length
- }
- if (typeof value === 'string') {
- return !!value
- }
- if (typeof value === 'object') {
- return (!value) ? false : !!Object.keys(value).length
- }
+ required ({ value }: ValidationContext, isRequired: string|boolean = true): boolean {
+ if (!isRequired || ['no', 'false'].includes(isRequired as string)) {
return true
- })())
+ }
+
+ if (Array.isArray(value)) {
+ return !!value.length
+ }
+
+ if (typeof value === 'string') {
+ return !!value
+ }
+
+ if (typeof value === 'object') {
+ return (!value) ? false : !!Object.keys(value).length
+ }
+
+ return true
},
/**
* Rule: Value starts with one of the given Strings
*/
- startsWith ({ value }: { value: any }, ...stack: string[]): Promise {
+ startsWith ({ value }: ValidationContext, ...stack: string[]): boolean {
if (!value) {
- return Promise.resolve(true)
+ return true
}
if (typeof value === 'string') {
- return Promise.resolve(stack.length === 0 || stack.some(str => value.startsWith(str)))
+ return stack.length === 0 || stack.some(str => value.startsWith(str))
}
- return Promise.resolve(false)
+ return false
},
/**
* Rule: checks if a string is a valid url
*/
- url ({ value }: { value: string }): Promise {
- return Promise.resolve(isUrl(value))
+ url ({ value }: ValidationContext): boolean {
+ return isUrl(value)
},
/**
* Rule: not a true rule — more like a compiler flag.
*/
- bail (): Promise {
- return Promise.resolve(true)
- }
+ bail (): boolean {
+ return true
+ },
}
+
+export default rules
diff --git a/src/validation/validator.ts b/src/validation/validator.ts
index 225dc43..2954a8e 100644
--- a/src/validation/validator.ts
+++ b/src/validation/validator.ts
@@ -11,14 +11,18 @@ export interface Violation {
message: string;
}
-export interface CheckRuleFn {
+export interface ValidationRuleFn {
(context: ValidationContext, ...args: any[]): Promise|boolean;
}
-export interface CreateMessageFn {
+export interface ValidationMessageFn {
(context: ValidationContext, ...args: any[]): string;
}
+export interface ValidationMessageI18NFn {
+ (vm: Vue, context: ValidationContext, ...args: any[]): string;
+}
+
export interface ValidationContext {
// The value of the field (do not mutate!),
value: any;
@@ -34,21 +38,20 @@ export type ValidatorGroup = {
}
export function createValidator (
- ruleFn: CheckRuleFn,
+ ruleFn: ValidationRuleFn,
ruleName: string|null,
ruleArgs: any[],
- messageFn: CreateMessageFn
+ messageFn: ValidationMessageFn
): Validator {
return (context: ValidationContext): Promise => {
- return Promise.resolve(ruleFn(context, ...ruleArgs))
- .then(valid => {
- return !valid ? {
- rule: ruleName,
- args: ruleArgs,
- context,
- message: messageFn(context, ...ruleArgs),
- } : null
- })
+ return Promise.resolve(ruleFn(context, ...ruleArgs)).then(valid => {
+ return !valid ? {
+ rule: ruleName,
+ args: ruleArgs,
+ context,
+ message: messageFn(context, ...ruleArgs),
+ } : null
+ })
}
}
@@ -61,8 +64,8 @@ export function parseModifier (ruleName: string): [string, string|null] {
export function processSingleArrayConstraint (
constraint: any[],
- rules: Record,
- messages: Record
+ rules: Record,
+ messages: Record
): [Validator, string|null, string|null] {
const args = constraint.slice()
const first = args.shift()
@@ -95,8 +98,8 @@ export function processSingleArrayConstraint (
export function processSingleStringConstraint (
constraint: string,
- rules: Record,
- messages: Record
+ rules: Record,
+ messages: Record
): [Validator, string|null, string|null] {
const args = constraint.split(':')
const [name, modifier] = parseModifier(args.shift() || '')
@@ -118,9 +121,9 @@ export function processSingleStringConstraint (
}
export function processSingleConstraint (
- constraint: string|Validator|[Validator|string, ...any[]],
- rules: Record,
- messages: Record
+ constraint: Validator|string|[Validator|string, ...any[]],
+ rules: Record,
+ messages: Record
): [Validator, string|null, string|null] {
if (typeof constraint === 'function') {
return [constraint, null, null]
@@ -139,8 +142,8 @@ export function processSingleConstraint (
export function processConstraints (
constraints: string|any[],
- rules: Record,
- messages: Record
+ rules: Record,
+ messages: Record
): [Validator, string|null, string|null][] {
if (typeof constraints === 'string') {
return processConstraints(constraints.split('|').filter(f => f.length), rules, messages)
diff --git a/storybook/stories/ExampleAddressList.tale.vue b/storybook/stories/ExampleAddressList.tale.vue
new file mode 100644
index 0000000..f010758
--- /dev/null
+++ b/storybook/stories/ExampleAddressList.tale.vue
@@ -0,0 +1,122 @@
+
+
+