refactor: Moved utils to separated files, code cleanup
This commit is contained in:
parent
e814edf9fc
commit
3f5735299d
@ -5,8 +5,8 @@ import messages from '@/validation/messages'
|
|||||||
import merge from '@/utils/merge'
|
import merge from '@/utils/merge'
|
||||||
|
|
||||||
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,
|
||||||
|
@ -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'
|
||||||
|
|
||||||
@ -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)
|
||||||
@ -193,6 +186,11 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
||||||
|
this.localFormErrors = formErrors || []
|
||||||
|
this.localFieldErrors = inputErrors || {}
|
||||||
|
}
|
||||||
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
hasValidationErrors (): Promise<boolean> {
|
||||||
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
|
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
|
||||||
resolvers.push(input.runValidation() && input.hasValidationErrors())
|
resolvers.push(input.runValidation() && input.hasValidationErrors())
|
||||||
@ -200,11 +198,6 @@ export default class FormularioForm extends Vue {
|
|||||||
}, [])).then(results => results.some(hasErrors => hasErrors))
|
}, [])).then(results => results.some(hasErrors => hasErrors))
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
|
||||||
this.localFormErrors = formErrors || []
|
|
||||||
this.localFieldErrors = inputErrors || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetValidation (): void {
|
resetValidation (): void {
|
||||||
this.localFormErrors = []
|
this.localFormErrors = []
|
||||||
this.localFieldErrors = {}
|
this.localFieldErrors = {}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
Prop,
|
Prop,
|
||||||
Watch,
|
Watch,
|
||||||
} from 'vue-property-decorator'
|
} from 'vue-property-decorator'
|
||||||
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './libs/utils'
|
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './utils'
|
||||||
import {
|
import {
|
||||||
CheckRuleFn,
|
CheckRuleFn,
|
||||||
CreateMessageFn,
|
CreateMessageFn,
|
||||||
|
@ -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).
|
||||||
*/
|
*/
|
||||||
@ -137,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,203 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,5 +1,5 @@
|
|||||||
import isUrl from 'is-url'
|
import isUrl from 'is-url'
|
||||||
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
import { shallowEqualObjects, regexForFormat, has } from '@/utils'
|
||||||
import { ValidationContext } from '@/validation/validator'
|
import { ValidationContext } from '@/validation/validator'
|
||||||
|
|
||||||
interface DateValidationContext extends ValidationContext {
|
interface DateValidationContext extends ValidationContext {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { has, snakeToCamel } from '@/libs/utils'
|
import { has, snakeToCamel } from '@/utils'
|
||||||
|
|
||||||
export interface Validator {
|
export interface Validator {
|
||||||
(context: ValidationContext): Promise<Violation|null>;
|
(context: ValidationContext): Promise<Violation|null>;
|
||||||
@ -59,7 +59,7 @@ export function parseModifier (ruleName: string): [string, string|null] {
|
|||||||
return [snakeToCamel(ruleName), null]
|
return [snakeToCamel(ruleName), null]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processArrayConstraint (
|
export function processSingleArrayConstraint (
|
||||||
constraint: any[],
|
constraint: any[],
|
||||||
rules: Record<string, CheckRuleFn>,
|
rules: Record<string, CheckRuleFn>,
|
||||||
messages: Record<string, CreateMessageFn>
|
messages: Record<string, CreateMessageFn>
|
||||||
@ -93,7 +93,7 @@ export function processArrayConstraint (
|
|||||||
throw new Error(`[Formulario] Can't create validator for constraint: ${JSON.stringify(constraint)}`)
|
throw new Error(`[Formulario] Can't create validator for constraint: ${JSON.stringify(constraint)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processStringConstraint (
|
export function processSingleStringConstraint (
|
||||||
constraint: string,
|
constraint: string,
|
||||||
rules: Record<string, CheckRuleFn>,
|
rules: Record<string, CheckRuleFn>,
|
||||||
messages: Record<string, CreateMessageFn>
|
messages: Record<string, CreateMessageFn>
|
||||||
@ -117,12 +117,8 @@ export function processStringConstraint (
|
|||||||
throw new Error(`[Formulario] Can't create validator for constraint: ${constraint}`)
|
throw new Error(`[Formulario] Can't create validator for constraint: ${constraint}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function processSingleConstraint (
|
||||||
* Given a string or function, parse it and return an array in the format
|
constraint: string|Validator|[Validator|string, ...any[]],
|
||||||
* [fn, [...arguments]]
|
|
||||||
*/
|
|
||||||
export function processConstraint (
|
|
||||||
constraint: any,
|
|
||||||
rules: Record<string, CheckRuleFn>,
|
rules: Record<string, CheckRuleFn>,
|
||||||
messages: Record<string, CreateMessageFn>
|
messages: Record<string, CreateMessageFn>
|
||||||
): [Validator, string|null, string|null] {
|
): [Validator, string|null, string|null] {
|
||||||
@ -131,11 +127,11 @@ export function processConstraint (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(constraint) && constraint.length) {
|
if (Array.isArray(constraint) && constraint.length) {
|
||||||
return processArrayConstraint(constraint, rules, messages)
|
return processSingleArrayConstraint(constraint, rules, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof constraint === 'string') {
|
if (typeof constraint === 'string') {
|
||||||
return processStringConstraint(constraint, rules, messages)
|
return processSingleStringConstraint(constraint, rules, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
return [(): Promise<Violation|null> => Promise.resolve(null), null, null]
|
return [(): Promise<Violation|null> => Promise.resolve(null), null, null]
|
||||||
@ -152,7 +148,7 @@ export function processConstraints (
|
|||||||
if (!Array.isArray(constraints)) {
|
if (!Array.isArray(constraints)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return constraints.map(constraint => processConstraint(constraint, rules, messages))
|
return constraints.map(constraint => processSingleConstraint(constraint, rules, messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
import { cloneDeep, isScalar, regexForFormat, snakeToCamel } from '@/libs/utils'
|
|
||||||
|
|
||||||
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
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user