chore!: Removed uploading functionality, dead code removals
This commit is contained in:
parent
434b288a3c
commit
cc5bb03bae
@ -1,231 +0,0 @@
|
||||
import nanoid from 'nanoid/non-secure'
|
||||
import { AxiosResponse, AxiosError } from '@/axios.types'
|
||||
|
||||
interface FileItem {
|
||||
uuid: string;
|
||||
name: string;
|
||||
path: string | false;
|
||||
progress: number | false;
|
||||
error: any | false;
|
||||
complete: boolean;
|
||||
file: File;
|
||||
justFinished: boolean;
|
||||
removeFile(): void;
|
||||
previewData: string | false;
|
||||
}
|
||||
|
||||
interface ProgressSetter {
|
||||
(progress: number): void;
|
||||
}
|
||||
|
||||
interface ErrorHandler {
|
||||
(error: AxiosError): any;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* The file upload class holds and represents a file’s upload state durring
|
||||
* the upload flow.
|
||||
*/
|
||||
class FileUpload {
|
||||
public input: DataTransfer
|
||||
public fileList: FileList
|
||||
public files: FileItem[]
|
||||
public options: Record<string, any>
|
||||
public context: Record<string, any>
|
||||
public results: any[] | boolean
|
||||
|
||||
constructor (input: DataTransfer, context: Record<string, any> = {}, options: Record<string, any> = {}) {
|
||||
this.input = input
|
||||
this.fileList = input.files
|
||||
this.files = []
|
||||
this.options = { mimes: {}, ...options }
|
||||
this.results = false
|
||||
this.context = context
|
||||
if (Array.isArray(this.fileList)) {
|
||||
this.rehydrateFileList(this.fileList)
|
||||
} else {
|
||||
this.addFileList(this.fileList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a pre-existing array of files, create a faux FileList.
|
||||
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
|
||||
*/
|
||||
rehydrateFileList (items: any[]): void {
|
||||
const fauxFileList = items.reduce((fileList, item) => {
|
||||
const key = this.options ? this.options.fileUrlKey : 'url'
|
||||
const url = item[key]
|
||||
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
|
||||
const mime = this.options.mimes[ext] || false
|
||||
fileList.push(Object.assign({}, item, url ? {
|
||||
name: url.substr((url.lastIndexOf('/') + 1) || 0),
|
||||
type: item.type ? item.type : mime,
|
||||
previewData: url
|
||||
} : {}))
|
||||
return fileList
|
||||
}, [])
|
||||
this.results = items
|
||||
this.addFileList(fauxFileList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce an array of files and alert the callback.
|
||||
* @param {FileList} fileList
|
||||
*/
|
||||
addFileList (fileList: FileList): void {
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file: File = fileList[i]
|
||||
const uuid = nanoid()
|
||||
this.files.push({
|
||||
progress: false,
|
||||
error: false,
|
||||
complete: false,
|
||||
justFinished: false,
|
||||
name: file.name || 'file-upload',
|
||||
file,
|
||||
uuid,
|
||||
path: false,
|
||||
removeFile: () => this.removeFile(uuid),
|
||||
// @ts-ignore
|
||||
previewData: file.previewData || false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file has an.
|
||||
*/
|
||||
hasUploader (): boolean {
|
||||
return !!this.context.uploader
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given uploader is axios instance. This isn't a great way of
|
||||
* testing if it is or not, but AFIK there isn't a better way right now:
|
||||
*
|
||||
* https://github.com/axios/axios/issues/737
|
||||
*/
|
||||
uploaderIsAxios (): boolean {
|
||||
return this.hasUploader &&
|
||||
typeof this.context.uploader.request === 'function' &&
|
||||
typeof this.context.uploader.get === 'function' &&
|
||||
typeof this.context.uploader.delete === 'function' &&
|
||||
typeof this.context.uploader.post === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new uploader function.
|
||||
*/
|
||||
getUploader (...args: [File, ProgressSetter, ErrorHandler, Record<string, any>]) {
|
||||
if (this.uploaderIsAxios()) {
|
||||
const data = new FormData()
|
||||
data.append(this.context.name || 'file', args[0])
|
||||
if (this.context.uploadUrl === false) {
|
||||
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
|
||||
}
|
||||
return this.context.uploader.post(this.context.uploadUrl, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (event: ProgressEvent) => {
|
||||
// args[1] here is the upload progress handler function
|
||||
args[1](Math.round((event.loaded * 100) / event.total))
|
||||
}
|
||||
})
|
||||
.then((response: AxiosResponse) => response.data)
|
||||
.catch(args[2])
|
||||
}
|
||||
return this.context.uploader(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the file upload.
|
||||
*/
|
||||
upload () {
|
||||
if (this.results) {
|
||||
return Promise.resolve(this.results)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.hasUploader) {
|
||||
return reject(new Error('No uploader has been defined'))
|
||||
}
|
||||
Promise.all(this.files.map(file => {
|
||||
return file.path ? Promise.resolve(file.path) : this.getUploader(
|
||||
file.file,
|
||||
progress => {
|
||||
file.progress = progress
|
||||
if (progress >= 100) {
|
||||
if (!file.complete) {
|
||||
file.justFinished = true
|
||||
setTimeout(() => { file.justFinished = false }, this.options.uploadJustCompleteDuration)
|
||||
}
|
||||
file.complete = true
|
||||
}
|
||||
},
|
||||
error => {
|
||||
file.progress = 0
|
||||
file.error = error
|
||||
file.complete = true
|
||||
},
|
||||
this.options
|
||||
)
|
||||
}))
|
||||
.then(results => {
|
||||
this.results = results
|
||||
resolve(results)
|
||||
})
|
||||
.catch(err => { throw new Error(err) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file from the uploader (and the file list)
|
||||
*/
|
||||
removeFile (uuid: string): void {
|
||||
this.files = this.files.filter(file => file.uuid !== uuid)
|
||||
this.context.performValidation()
|
||||
if (window && this.fileList instanceof FileList) {
|
||||
const transfer = new DataTransfer()
|
||||
this.files.map(({ file }) => transfer.items.add(file))
|
||||
this.fileList = transfer.files
|
||||
this.input = transfer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* load image previews for all uploads.
|
||||
*/
|
||||
loadPreviews () {
|
||||
this.files.map(file => {
|
||||
if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) {
|
||||
const reader = new FileReader()
|
||||
// @ts-ignore
|
||||
reader.onload = e => Object.assign(file, { previewData: e.target.result })
|
||||
reader.readAsDataURL(file.file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files.
|
||||
*/
|
||||
getFileList () {
|
||||
return this.fileList
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files.
|
||||
*/
|
||||
getFiles () {
|
||||
return this.files
|
||||
}
|
||||
|
||||
toString (): string {
|
||||
const descriptor = this.files.length ? this.files.length + ' files' : 'empty'
|
||||
return this.results ? JSON.stringify(this.results, null, ' ') : `FileUpload(${descriptor})`
|
||||
}
|
||||
}
|
||||
|
||||
export default FileUpload
|
@ -1,31 +1,22 @@
|
||||
import { VueConstructor } from 'vue'
|
||||
|
||||
import mimes from '@/libs/mimes'
|
||||
import { has } from '@/libs/utils'
|
||||
import fauxUploader from '@/libs/faux-uploader'
|
||||
import rules from '@/validation/rules'
|
||||
import messages from '@/validation/messages'
|
||||
import merge from '@/utils/merge'
|
||||
|
||||
import FileUpload from '@/FileUpload'
|
||||
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
import FormularioGrouping from '@/FormularioGrouping.vue'
|
||||
|
||||
import { ValidationContext, ValidationRule } from '@/validation/types'
|
||||
import {
|
||||
ValidationContext,
|
||||
ValidationRule,
|
||||
} from '@/validation/types'
|
||||
|
||||
interface FormularioOptions {
|
||||
components?: { [name: string]: VueConstructor };
|
||||
plugins?: any[];
|
||||
rules?: any;
|
||||
mimes?: any;
|
||||
uploader?: any;
|
||||
uploadUrl?: any;
|
||||
fileUrlKey?: any;
|
||||
uploadJustCompleteDuration?: any;
|
||||
validationMessages?: any;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
@ -38,19 +29,8 @@ export default class Formulario {
|
||||
|
||||
constructor () {
|
||||
this.options = {
|
||||
components: {
|
||||
FormularioForm,
|
||||
FormularioInput,
|
||||
FormularioGrouping,
|
||||
},
|
||||
rules,
|
||||
mimes,
|
||||
uploader: fauxUploader,
|
||||
uploadUrl: false,
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
validationMessages: messages,
|
||||
idPrefix: 'formulario-'
|
||||
}
|
||||
this.idRegistry = {}
|
||||
}
|
||||
@ -60,12 +40,11 @@ export default class Formulario {
|
||||
*/
|
||||
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 || {})
|
||||
for (const componentName in this.options.components) {
|
||||
if (has(this.options.components, componentName)) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,13 +54,12 @@ export default class Formulario {
|
||||
* implementation is open to community review.
|
||||
*/
|
||||
nextId (vm: Vue): string {
|
||||
const options = this.options as FormularioOptions
|
||||
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 `${options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||
return `formulario-${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,33 +90,4 @@ export default class Formulario {
|
||||
return this.options.validationMessages.default(vm, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file uploader.
|
||||
*/
|
||||
getUploader (): any {
|
||||
return this.options.uploader || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global upload url.
|
||||
*/
|
||||
getUploadUrl (): string | boolean {
|
||||
return this.options.uploadUrl || false
|
||||
}
|
||||
|
||||
/**
|
||||
* When re-hydrating a file uploader with an array, get the sub-object key to
|
||||
* access the url of the file. Usually this is just "url".
|
||||
*/
|
||||
getFileUrlKey (): string {
|
||||
return this.options.fileUrlKey || 'url'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of an upload.
|
||||
*/
|
||||
createUpload (data: DataTransfer, context: Record<string, any>): FileUpload {
|
||||
return new FileUpload(data, context, this.options)
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,9 @@ import {
|
||||
Provide,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import { arrayify, cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
||||
import Registry from '@/libs/registry'
|
||||
import { cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
||||
import merge from '@/utils/merge'
|
||||
import Registry from '@/form/registry'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
import {
|
||||
@ -25,41 +26,21 @@ import {
|
||||
|
||||
import { ValidationErrorBag } from '@/validation/types'
|
||||
|
||||
import FileUpload from '@/FileUpload'
|
||||
|
||||
@Component({ name: 'FormularioForm' })
|
||||
export default class FormularioForm extends Vue {
|
||||
@Provide() formularioFieldValidation (errorBag: ValidationErrorBag): void {
|
||||
this.$emit('validation', errorBag)
|
||||
}
|
||||
|
||||
@Provide() getFormValues = (): Record<string, any> => this.proxy
|
||||
@Provide() path = ''
|
||||
|
||||
@Model('input', {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}) readonly formularioValue!: Record<string, any>
|
||||
|
||||
@Prop({
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
}) public readonly name!: string | boolean
|
||||
|
||||
@Prop({
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
}) readonly values!: Record<string, any> | boolean
|
||||
@Model('input', { default: () => ({}) })
|
||||
public readonly formularioValue!: Record<string, any>
|
||||
|
||||
@Prop({ default: () => ({}) }) readonly errors!: Record<string, any>
|
||||
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
||||
|
||||
@Provide()
|
||||
public path = ''
|
||||
|
||||
public proxy: Record<string, any> = {}
|
||||
|
||||
registry: Registry = new Registry(this)
|
||||
|
||||
childrenShouldShowErrors = false
|
||||
|
||||
private errorObserverRegistry = new ErrorObserverRegistry()
|
||||
private localFormErrors: string[] = []
|
||||
private localFieldErrors: Record<string, string[]> = {}
|
||||
@ -68,35 +49,16 @@ export default class FormularioForm extends Vue {
|
||||
return [...this.formErrors, ...this.localFormErrors]
|
||||
}
|
||||
|
||||
get mergedFieldErrors (): Record<string, any> {
|
||||
const errors: Record<string, any> = {}
|
||||
|
||||
if (this.errors) {
|
||||
for (const fieldName in this.errors) {
|
||||
errors[fieldName] = arrayify(this.errors[fieldName])
|
||||
}
|
||||
}
|
||||
|
||||
for (const fieldName in this.localFieldErrors) {
|
||||
errors[fieldName] = arrayify(this.localFieldErrors[fieldName])
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
get hasInitialValue (): boolean {
|
||||
return (
|
||||
(this.formularioValue && typeof this.formularioValue === 'object') ||
|
||||
(this.values && typeof this.values === 'object')
|
||||
)
|
||||
get mergedFieldErrors (): Record<string, string[]> {
|
||||
return merge(this.errors || {}, this.localFieldErrors)
|
||||
}
|
||||
|
||||
get hasModel (): boolean {
|
||||
return has(this.$options.propsData || {}, 'formularioValue')
|
||||
}
|
||||
|
||||
get hasValue (): boolean {
|
||||
return has(this.$options.propsData || {}, 'values')
|
||||
get hasInitialValue (): boolean {
|
||||
return this.formularioValue && typeof this.formularioValue === 'object'
|
||||
}
|
||||
|
||||
get initialValues (): Record<string, any> {
|
||||
@ -105,11 +67,6 @@ export default class FormularioForm extends Vue {
|
||||
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
||||
}
|
||||
|
||||
if (this.hasValue && typeof this.values === 'object') {
|
||||
// If there are values, use them as secondary priority
|
||||
return { ...this.values }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
@ -134,14 +91,14 @@ export default class FormularioForm extends Vue {
|
||||
this.initProxy()
|
||||
}
|
||||
|
||||
onFormSubmit (): Promise<void> {
|
||||
this.childrenShouldShowErrors = true
|
||||
this.registry.forEach((input: FormularioInput) => {
|
||||
input.formShouldShowErrors = true
|
||||
})
|
||||
@Provide()
|
||||
getFormValues (): Record<string, any> {
|
||||
return this.proxy
|
||||
}
|
||||
|
||||
onFormSubmit (): Promise<void> {
|
||||
return this.hasValidationErrors()
|
||||
.then(hasErrors => hasErrors ? undefined : this.getValues())
|
||||
.then(hasErrors => hasErrors ? undefined : cloneDeep(this.proxy))
|
||||
.then(data => {
|
||||
if (typeof data !== 'undefined') {
|
||||
this.$emit('submit', data)
|
||||
@ -151,6 +108,11 @@ export default class FormularioForm extends Vue {
|
||||
})
|
||||
}
|
||||
|
||||
@Provide()
|
||||
onFormularioFieldValidation (errorBag: ValidationErrorBag): void {
|
||||
this.$emit('validation', errorBag)
|
||||
}
|
||||
|
||||
@Provide()
|
||||
addErrorObserver (observer: ErrorObserver): void {
|
||||
this.errorObserverRegistry.add(observer)
|
||||
@ -176,67 +138,12 @@ export default class FormularioForm extends Vue {
|
||||
this.registry.remove(field)
|
||||
}
|
||||
|
||||
resetValidation (): void {
|
||||
this.localFormErrors = []
|
||||
this.localFieldErrors = {}
|
||||
this.childrenShouldShowErrors = false
|
||||
this.registry.forEach((input: FormularioInput) => {
|
||||
input.formShouldShowErrors = false
|
||||
input.behavioralErrorVisibility = false
|
||||
})
|
||||
}
|
||||
|
||||
initProxy (): void {
|
||||
if (this.hasInitialValue) {
|
||||
this.proxy = this.initialValues
|
||||
}
|
||||
}
|
||||
|
||||
@Provide('formularioSetter')
|
||||
setFieldValue (field: string, value: any, emit = true): void {
|
||||
if (value === undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [field]: value, ...proxy } = this.proxy
|
||||
this.proxy = proxy
|
||||
} else {
|
||||
setNested(this.proxy, field, value)
|
||||
}
|
||||
|
||||
if (emit) {
|
||||
this.$emit('input', Object.assign({}, this.proxy))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously generate the values payload of this form.
|
||||
*/
|
||||
getValues (): Promise<Record<string, any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pending = []
|
||||
const values = cloneDeep(this.proxy)
|
||||
|
||||
for (const key in values) {
|
||||
if (has(values, key) && typeof this.proxy[key] === 'object' && this.proxy[key] instanceof FileUpload) {
|
||||
pending.push(
|
||||
this.proxy[key].upload()
|
||||
.then((data: Record<string, any>) => Object.assign(values, { [key]: data }))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(pending)
|
||||
.then(() => resolve(values))
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
setValues (values: Record<string, any>): void {
|
||||
const keys = Array.from(new Set([...Object.keys(values), ...Object.keys(this.proxy)]))
|
||||
let proxyHasChanges = false
|
||||
@ -266,10 +173,40 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Provide('formularioSetter')
|
||||
setFieldValue (field: string, value: any, emit = true): void {
|
||||
if (value === undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [field]: value, ...proxy } = this.proxy
|
||||
this.proxy = proxy
|
||||
} else {
|
||||
setNested(this.proxy, field, value)
|
||||
}
|
||||
|
||||
if (emit) {
|
||||
this.$emit('input', Object.assign({}, this.proxy))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// given an object of errors, apply them to this form
|
||||
this.localFormErrors = formErrors || []
|
||||
this.localFieldErrors = inputErrors || {}
|
||||
}
|
||||
|
||||
resetValidation (): void {
|
||||
this.localFormErrors = []
|
||||
this.localFieldErrors = {}
|
||||
this.registry.forEach((input: FormularioInput) => {
|
||||
input.resetValidation()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulario-group"
|
||||
data-type="group"
|
||||
>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulario-input"
|
||||
:data-has-errors="hasErrors"
|
||||
:data-is-showing-errors="hasVisibleErrors"
|
||||
:data-type="type"
|
||||
>
|
||||
<div class="formulario-input">
|
||||
<slot
|
||||
:id="id"
|
||||
:context="context"
|
||||
:errors="errors"
|
||||
:validationErrors="validationErrors"
|
||||
:violations="validationErrors"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -23,11 +17,10 @@ import {
|
||||
Prop,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify } from './libs/utils'
|
||||
import { arrayify, has, parseRules, shallowEqualObjects, snakeToCamel } from './libs/utils'
|
||||
import {
|
||||
ValidationContext,
|
||||
ValidationError,
|
||||
ValidationErrorBag,
|
||||
ValidationRule,
|
||||
} from '@/validation/types'
|
||||
import {
|
||||
@ -46,7 +39,7 @@ const ERROR_BEHAVIOR = {
|
||||
@Component({ name: 'FormularioInput', inheritAttrs: false })
|
||||
export default class FormularioInput extends Vue {
|
||||
@Inject({ default: undefined }) formularioSetter!: Function|undefined
|
||||
@Inject({ default: () => (): void => {} }) formularioFieldValidation!: Function
|
||||
@Inject({ default: () => (): void => {} }) onFormularioFieldValidation!: Function
|
||||
@Inject({ default: undefined }) formularioRegister!: Function|undefined
|
||||
@Inject({ default: undefined }) formularioDeregister!: Function|undefined
|
||||
@Inject({ default: () => (): Record<string, any> => ({}) }) getFormValues!: Function
|
||||
@ -56,68 +49,45 @@ export default class FormularioInput extends Vue {
|
||||
|
||||
@Model('input', { default: '' }) formularioValue: any
|
||||
|
||||
@Prop({
|
||||
type: [String, Number, Boolean],
|
||||
default: false,
|
||||
}) id!: string|number|boolean
|
||||
|
||||
@Prop({ default: 'text' }) type!: string
|
||||
@Prop({ default: null }) id!: string|number|null
|
||||
@Prop({ required: true }) name!: string
|
||||
@Prop({ default: false }) value!: any
|
||||
|
||||
@Prop({
|
||||
default: '',
|
||||
}) validation!: string|any[]
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}) validationRules!: Record<string, ValidationRule>
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}) validationMessages!: Record<string, any>
|
||||
|
||||
@Prop({ default: '' }) validation!: string|any[]
|
||||
@Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRule>
|
||||
@Prop({ default: () => ({}) }) validationMessages!: Record<string, any>
|
||||
@Prop({ default: () => [] }) errors!: string[]
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
default: ERROR_BEHAVIOR.BLUR,
|
||||
validator: value => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(value)
|
||||
validator: behavior => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(behavior)
|
||||
}) errorBehavior!: string
|
||||
|
||||
@Prop({ default: false }) showErrors!: boolean
|
||||
@Prop({ default: false }) disableErrors!: boolean
|
||||
@Prop({ default: true }) preventWindowDrops!: boolean
|
||||
@Prop({ default: 'preview' }) imageBehavior!: string
|
||||
@Prop({ default: false }) uploader!: Function|Record<string, any>|boolean
|
||||
@Prop({ default: false }) uploadUrl!: string|boolean
|
||||
@Prop({ default: 'live' }) uploadBehavior!: string
|
||||
|
||||
defaultId: string = this.$formulario.nextId(this)
|
||||
localErrors: string[] = []
|
||||
proxy: Record<string, any> = this.getInitialValue()
|
||||
behavioralErrorVisibility: boolean = this.errorBehavior === 'live'
|
||||
formShouldShowErrors = false
|
||||
localErrors: string[] = []
|
||||
validationErrors: ValidationError[] = []
|
||||
pendingValidation: Promise<any> = Promise.resolve()
|
||||
|
||||
get context (): Record<string, any> {
|
||||
return this.defineModel({
|
||||
return Object.defineProperty({
|
||||
id: this.id || this.defaultId,
|
||||
name: this.nameOrFallback,
|
||||
blurHandler: this.blurHandler.bind(this),
|
||||
errors: this.explicitErrors,
|
||||
allErrors: this.allErrors,
|
||||
formShouldShowErrors: this.formShouldShowErrors,
|
||||
imageBehavior: this.imageBehavior,
|
||||
performValidation: this.performValidation.bind(this),
|
||||
showValidationErrors: this.showValidationErrors,
|
||||
uploader: this.uploader || this.$formulario.getUploader(),
|
||||
validationErrors: this.validationErrors,
|
||||
value: this.value,
|
||||
visibleValidationErrors: this.visibleValidationErrors,
|
||||
}, 'model', {
|
||||
get: this.modelGetter.bind(this),
|
||||
set: this.modelSetter.bind(this),
|
||||
})
|
||||
}
|
||||
|
||||
@ -151,13 +121,6 @@ export default class FormularioInput extends Vue {
|
||||
return this.allErrors.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if form has actively visible errors (of any kind)
|
||||
*/
|
||||
get hasVisibleErrors (): boolean {
|
||||
return (this.validationErrors && this.showValidationErrors) || this.explicitErrors.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged errors computed property.
|
||||
* Each error is an object with fields message (translated message), rule (rule name) and context
|
||||
@ -169,13 +132,6 @@ export default class FormularioInput extends Vue {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* All of the currently visible validation errors (does not include error handling)
|
||||
*/
|
||||
get visibleValidationErrors (): ValidationError[] {
|
||||
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
|
||||
}
|
||||
|
||||
/**
|
||||
* These are errors we that have been explicitly passed to us.
|
||||
*/
|
||||
@ -190,13 +146,6 @@ export default class FormularioInput extends Vue {
|
||||
return has(this.$options.propsData || {}, 'formularioValue')
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the field should show it's error (if it has one)
|
||||
*/
|
||||
get showValidationErrors (): boolean {
|
||||
return this.showErrors || this.formShouldShowErrors || this.behavioralErrorVisibility
|
||||
}
|
||||
|
||||
@Watch('proxy')
|
||||
onProxyChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void {
|
||||
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
||||
@ -216,11 +165,6 @@ export default class FormularioInput extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('showValidationErrors', { immediate: true })
|
||||
onShowValidationErrorsChanged (val: boolean): void {
|
||||
this.$emit('error-visibility', val)
|
||||
}
|
||||
|
||||
created (): void {
|
||||
this.applyInitialValue()
|
||||
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
||||
@ -244,16 +188,6 @@ export default class FormularioInput extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the model used throughout the existing context.
|
||||
*/
|
||||
defineModel (context: Record<string, any>): Record<string, any> {
|
||||
return Object.defineProperty(context, 'model', {
|
||||
get: this.modelGetter.bind(this),
|
||||
set: this.modelSetter.bind(this),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value from a model.
|
||||
*/
|
||||
@ -283,8 +217,8 @@ export default class FormularioInput extends Vue {
|
||||
*/
|
||||
blurHandler (): void {
|
||||
this.$emit('blur')
|
||||
if (this.errorBehavior === 'blur') {
|
||||
this.behavioralErrorVisibility = true
|
||||
if (this.errorBehavior === ERROR_BEHAVIOR.BLUR) {
|
||||
this.performValidation()
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,10 +286,13 @@ export default class FormularioInput extends Vue {
|
||||
const validationChanged = !shallowEqualObjects(violations, this.validationErrors)
|
||||
this.validationErrors = violations
|
||||
if (validationChanged) {
|
||||
const errorBag = this.getErrorObject()
|
||||
const errorBag = {
|
||||
name: this.context.name,
|
||||
errors: this.validationErrors,
|
||||
}
|
||||
this.$emit('validation', errorBag)
|
||||
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
|
||||
this.formularioFieldValidation(errorBag)
|
||||
if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') {
|
||||
this.onFormularioFieldValidation(errorBag)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -398,16 +335,13 @@ export default class FormularioInput extends Vue {
|
||||
})
|
||||
}
|
||||
|
||||
getErrorObject (): ValidationErrorBag {
|
||||
return {
|
||||
name: this.context.nameOrFallback || this.context.name,
|
||||
errors: this.validationErrors.filter(s => typeof s === 'object'),
|
||||
hasErrors: !!this.validationErrors.length
|
||||
}
|
||||
}
|
||||
|
||||
setErrors (errors: string[]): void {
|
||||
this.localErrors = arrayify(errors)
|
||||
}
|
||||
|
||||
resetValidation (): void {
|
||||
this.localErrors = []
|
||||
this.validationErrors = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,5 +0,0 @@
|
||||
export interface AxiosResponse {
|
||||
data: any
|
||||
}
|
||||
|
||||
export interface AxiosError {}
|
@ -1,4 +1,4 @@
|
||||
import { shallowEqualObjects, has, getNested } from './utils'
|
||||
import { shallowEqualObjects, has, getNested } from '@/libs/utils'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
@ -1,38 +0,0 @@
|
||||
interface UploadedFile {
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake uploader used by default.
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {function} progress
|
||||
* @param {function} error
|
||||
* @param {object} options
|
||||
*/
|
||||
export default function (file: any, progress: any, error: any, options: any): Promise<UploadedFile> {
|
||||
return new Promise(resolve => {
|
||||
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
|
||||
const start = performance.now()
|
||||
|
||||
/**
|
||||
* Create a recursive timeout that advances the progress.
|
||||
*/
|
||||
const advance = () => setTimeout(() => {
|
||||
const elapsed = performance.now() - start
|
||||
const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100))
|
||||
progress(currentProgress)
|
||||
|
||||
if (currentProgress >= 100) {
|
||||
return resolve({
|
||||
url: 'http://via.placeholder.com/350x150.png',
|
||||
name: file.name
|
||||
})
|
||||
} else {
|
||||
advance()
|
||||
}
|
||||
}, 20)
|
||||
advance()
|
||||
})
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export default {
|
||||
csv: 'text/csv',
|
||||
gif: 'image/gif',
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
pdf: 'application/pdf',
|
||||
svg: 'image/svg+xml'
|
||||
}
|
@ -1,20 +1,3 @@
|
||||
import FileUpload from '@/FileUpload'
|
||||
|
||||
/**
|
||||
* Function to map over an object.
|
||||
* @param {Object} original An object to map over
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function map (original: Record<string, any>, callback: Function): Record<string, any> {
|
||||
const obj: Record<string, any> = {}
|
||||
for (const key in original) {
|
||||
if (Object.prototype.hasOwnProperty.call(original, key)) {
|
||||
obj[key] = callback(key, original[key])
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
||||
if (objA === objB) {
|
||||
return true
|
||||
@ -148,57 +131,6 @@ export function parseRules (validation: any[]|string, rules: any): any[] {
|
||||
}).filter(f => !!f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of rules, group them by bail signals. For example for this:
|
||||
* bail|required|min:10|max:20
|
||||
* we would expect:
|
||||
* [[required], [min], [max]]
|
||||
* because any sub-array failure would cause a shutdown. While
|
||||
* ^required|min:10|max:10
|
||||
* would return:
|
||||
* [[required], [min, max]]
|
||||
* and no bailing would produce:
|
||||
* [[required, min, max]]
|
||||
*/
|
||||
export function groupBails (rules: any[]): any[] {
|
||||
const groups = []
|
||||
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||
if (bailIndex >= 0) {
|
||||
// Get all the rules until the first bail rule (dont include the bail)
|
||||
const preBail = rules.splice(0, bailIndex + 1).slice(0, -1)
|
||||
// Rules before the `bail` rule are non-bailing
|
||||
preBail.length && groups.push(preBail)
|
||||
// All remaining rules are bailing rule groups
|
||||
rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true })))
|
||||
} else {
|
||||
groups.push(rules)
|
||||
}
|
||||
|
||||
return groups.reduce((groups, group) => {
|
||||
// @ts-ignore
|
||||
const splitByMod = (group, bailGroup = false) => {
|
||||
if (group.length < 2) {
|
||||
return Object.defineProperty([group], 'bail', { value: bailGroup })
|
||||
}
|
||||
const splits = []
|
||||
// @ts-ignore
|
||||
const modIndex = group.findIndex(([,,, modifier]) => modifier === '^')
|
||||
if (modIndex >= 0) {
|
||||
const preMod = group.splice(0, modIndex)
|
||||
// rules before the modifier are non-bailing rules.
|
||||
preMod.length && splits.push(...splitByMod(preMod, bailGroup))
|
||||
splits.push(Object.defineProperty([group.shift()], 'bail', { value: true }))
|
||||
// rules after the modifier are non-bailing rules.
|
||||
group.length && splits.push(...splitByMod(group, bailGroup))
|
||||
} else {
|
||||
splits.push(group)
|
||||
}
|
||||
return splits
|
||||
}
|
||||
return groups.concat(splitByMod(group))
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for use in regular expressions.
|
||||
*/
|
||||
@ -255,7 +187,7 @@ export function cloneDeep (value: any): any {
|
||||
|
||||
for (const key in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
if (isScalar(value[key]) || value[key] instanceof FileUpload) {
|
||||
if (isScalar(value[key])) {
|
||||
copy[key] = value[key]
|
||||
} else {
|
||||
copy[key] = cloneDeep(value[key])
|
||||
|
@ -1,12 +1,7 @@
|
||||
// @ts-ignore
|
||||
import isUrl from 'is-url'
|
||||
import FileUpload from '../FileUpload'
|
||||
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
||||
import { ValidatableData } from '@/validation/types'
|
||||
|
||||
/**
|
||||
* Library of rules
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Rule: the value must be "yes", "on", "1", or true
|
||||
@ -155,21 +150,6 @@ export default {
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the file type is correct.
|
||||
*/
|
||||
mime ({ value }: { value: any }, ...types: string[]): Promise<boolean> {
|
||||
if (value instanceof FileUpload) {
|
||||
const files = value.getFiles()
|
||||
const isMimeCorrect = (file: File): boolean => types.includes(file.type)
|
||||
const allValid: boolean = files.reduce((valid: boolean, { file }) => valid && isMimeCorrect(file), true)
|
||||
|
||||
return Promise.resolve(allValid)
|
||||
}
|
||||
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the minimum value of a particular.
|
||||
*/
|
||||
@ -239,9 +219,6 @@ export default {
|
||||
if (Array.isArray(value)) {
|
||||
return !!value.length
|
||||
}
|
||||
if (value instanceof FileUpload) {
|
||||
return value.getFiles().length > 0
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return !!value
|
||||
}
|
||||
|
@ -31,5 +31,4 @@ export interface ValidationError {
|
||||
export interface ValidationErrorBag {
|
||||
name: string;
|
||||
errors: ValidationError[];
|
||||
hasErrors: boolean;
|
||||
}
|
||||
|
@ -2,11 +2,6 @@ import Formulario from '@/index.ts'
|
||||
|
||||
describe('Formulario', () => {
|
||||
it('Installs on vue instance', () => {
|
||||
const components = [
|
||||
'FormularioForm',
|
||||
'FormularioInput',
|
||||
'FormularioGrouping',
|
||||
]
|
||||
const registry = []
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {
|
||||
@ -14,6 +9,10 @@ describe('Formulario', () => {
|
||||
}
|
||||
Formulario.install(Vue)
|
||||
expect(Vue.prototype.$formulario).toBe(Formulario)
|
||||
expect(registry).toEqual(components)
|
||||
expect(registry).toEqual([
|
||||
'FormularioForm',
|
||||
'FormularioGrouping',
|
||||
'FormularioInput',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@ -214,20 +214,7 @@ describe('FormularioForm', () => {
|
||||
|
||||
expect(emitted['submit']).toBeTruthy()
|
||||
expect(emitted['submit'].length).toBe(1)
|
||||
expect(emitted['submit'][0]).toStrictEqual([{ fieldName: 'Justin' }])
|
||||
})
|
||||
|
||||
it('accepts a values prop and uses it to set the initial values', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { values: { name: 'Dave Barnett', candy: true } },
|
||||
slots: { default: `
|
||||
<FormularioInput v-slot="{ context }" name="name" validation="required">
|
||||
<input v-model="context.model" type="text">
|
||||
</FormularioInput>
|
||||
` }
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('input[type="text"]').element['value']).toBe('Dave Barnett')
|
||||
expect(emitted['submit'][0]).toEqual([{ fieldName: 'Justin' }])
|
||||
})
|
||||
|
||||
it('Receives a form-errors prop and displays it', async () => {
|
||||
|
@ -10,7 +10,6 @@ Vue.use(Formulario)
|
||||
describe('FormularioGrouping', () => {
|
||||
it('Grouped fields to be set', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { name: 'form' },
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioGrouping name="group">
|
||||
@ -33,17 +32,16 @@ describe('FormularioGrouping', () => {
|
||||
|
||||
expect(emitted['submit']).toBeTruthy()
|
||||
expect(emitted['submit'].length).toBe(1)
|
||||
expect(emitted['submit'][0]).toStrictEqual([{ group: { text: 'test' } }])
|
||||
expect(emitted['submit'][0]).toEqual([{ group: { text: 'test' } }])
|
||||
})
|
||||
|
||||
it('Grouped fields to be got', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: {
|
||||
name: 'form',
|
||||
formularioValue: {
|
||||
group: { text: 'Group text' },
|
||||
text: 'Text',
|
||||
}
|
||||
},
|
||||
},
|
||||
slots: {
|
||||
default: `
|
||||
@ -79,23 +77,20 @@ describe('FormularioGrouping', () => {
|
||||
})
|
||||
|
||||
it('Errors are set for grouped fields', async () => {
|
||||
const wrapper = mount({
|
||||
data: () => ({ values: {} }),
|
||||
template: `
|
||||
<FormularioForm
|
||||
v-model="values"
|
||||
:errors="{'group.text': 'Test error'}"
|
||||
name="form"
|
||||
>
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: {
|
||||
formularioValue: {},
|
||||
errors: { 'group.text': 'Test error' },
|
||||
},
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioGrouping name="group">
|
||||
<FormularioInput name="text" v-slot="{ context }">
|
||||
<span v-for="error in context.allErrors">
|
||||
{{ error }}
|
||||
</span>
|
||||
<FormularioInput ref="input" name="text" v-slot="{ context }">
|
||||
<span v-for="error in context.allErrors">{{ error }}</span>
|
||||
</FormularioInput>
|
||||
</FormularioGrouping>
|
||||
</FormularioForm>
|
||||
`
|
||||
`,
|
||||
},
|
||||
})
|
||||
expect(wrapper.findAll('span').length).toBe(1)
|
||||
})
|
||||
|
@ -182,74 +182,20 @@ describe('FormularioInput', () => {
|
||||
const errorObject = wrapper.emitted('validation')[0][0]
|
||||
expect(errorObject).toEqual({
|
||||
name: 'testinput',
|
||||
errors: [
|
||||
{
|
||||
message: expect.any(String),
|
||||
errors: [{
|
||||
rule: expect.stringContaining('required'),
|
||||
context: expect.any(Object)
|
||||
}
|
||||
],
|
||||
hasErrors: true
|
||||
context: expect.any(Object),
|
||||
message: expect.any(String),
|
||||
}],
|
||||
})
|
||||
})
|
||||
|
||||
it('emits a error-visibility event on blur', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
validation: 'required',
|
||||
errorBehavior: 'blur',
|
||||
value: '',
|
||||
name: 'testinput',
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<input type="text" v-model="props.context.model" @blur="props.context.blurHandler">`
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('error-visibility')[0][0]).toBe(false)
|
||||
wrapper.find('input[type="text"]').trigger('blur')
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('error-visibility')[1][0]).toBe(true)
|
||||
})
|
||||
|
||||
it('emits error-visibility event immediately when live', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
validation: 'required',
|
||||
errorBehavior: 'live',
|
||||
value: '',
|
||||
name: 'testinput',
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('error-visibility').length).toBe(1)
|
||||
})
|
||||
|
||||
it('Does not emit an error-visibility event if visibility did not change', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
validation: 'in:xyz',
|
||||
errorBehavior: 'live',
|
||||
value: 'bar',
|
||||
name: 'testinput',
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<input type="text" v-model="props.context.model">`
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('error-visibility').length).toBe(1)
|
||||
wrapper.find('input[type="text"]').setValue('bar')
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('error-visibility').length).toBe(1)
|
||||
})
|
||||
|
||||
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, {
|
||||
propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
|
||||
})
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1);
|
||||
expect(wrapper.vm.context.validationErrors.length).toBe(1);
|
||||
})
|
||||
|
||||
it('can show multiple validation errors if they occur before the bail rule', async () => {
|
||||
@ -257,7 +203,7 @@ describe('FormularioInput', () => {
|
||||
propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
|
||||
})
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2);
|
||||
expect(wrapper.vm.context.validationErrors.length).toBe(2);
|
||||
})
|
||||
|
||||
it('can avoid bail behavior by using modifier', async () => {
|
||||
@ -265,7 +211,7 @@ describe('FormularioInput', () => {
|
||||
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' }
|
||||
})
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2);
|
||||
expect(wrapper.vm.context.validationErrors.length).toBe(2);
|
||||
})
|
||||
|
||||
it('prevents later error messages when modified rule fails', async () => {
|
||||
@ -273,7 +219,7 @@ describe('FormularioInput', () => {
|
||||
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' }
|
||||
})
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1);
|
||||
expect(wrapper.vm.context.validationErrors.length).toBe(1);
|
||||
})
|
||||
|
||||
it('can bail in the middle of the rule set with a modifier', async () => {
|
||||
@ -281,7 +227,7 @@ describe('FormularioInput', () => {
|
||||
propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' }
|
||||
})
|
||||
await flushPromises();
|
||||
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2);
|
||||
expect(wrapper.vm.context.validationErrors.length).toBe(2);
|
||||
})
|
||||
|
||||
it('does not show errors on blur when set error-behavior is submit', async () => {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { parseRules, parseLocale, regexForFormat, cloneDeep, isScalar, snakeToCamel, groupBails } from '@/libs/utils'
|
||||
import { cloneDeep, isScalar, parseRules, regexForFormat, snakeToCamel } from '@/libs/utils'
|
||||
import rules from '@/validation/rules.ts'
|
||||
import FileUpload from '@/FileUpload'
|
||||
|
||||
describe('parseRules', () => {
|
||||
it('parses single string rules, returning empty arguments array', () => {
|
||||
@ -113,8 +112,6 @@ describe('isScalar', () => {
|
||||
it('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||
|
||||
it('fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||
|
||||
it('fails on custom type', () => expect(isScalar(FileUpload)).toBe(false))
|
||||
})
|
||||
|
||||
describe('cloneDeep', () => {
|
||||
@ -175,83 +172,3 @@ describe('snakeToCamel', () => {
|
||||
expect(snakeToCamel(fn)).toBe(fn)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('parseLocale', () => {
|
||||
it('properly orders the options', () => {
|
||||
expect(parseLocale('en-US-VA')).toEqual(['en-US-VA', 'en-US', 'en'])
|
||||
})
|
||||
|
||||
it('properly parses a single option', () => {
|
||||
expect(parseLocale('en')).toEqual(['en'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('groupBails', () => {
|
||||
it('wraps non bailed rules in an array', () => {
|
||||
const bailGroups = groupBails([[,,'required'], [,,'min']])
|
||||
expect(bailGroups).toEqual(
|
||||
[ [[,,'required'], [,,'min']] ] // dont bail on either of these
|
||||
)
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([false])
|
||||
})
|
||||
|
||||
it('splits bailed rules into two arrays array', () => {
|
||||
const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'bail'], [,, 'matches'], [,,'min']])
|
||||
expect(bailGroups).toEqual([
|
||||
[ [,,'required'], [,,'max'] ], // dont bail on these
|
||||
[ [,, 'matches'] ], // bail on this one
|
||||
[ [,,'min'] ] // bail on this one
|
||||
])
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([false, true, true])
|
||||
})
|
||||
|
||||
it('splits entire rule set when bail is at the beginning', () => {
|
||||
const bailGroups = groupBails([[,, 'bail'], [,,'required'], [,,'max'], [,, 'matches'], [,,'min']])
|
||||
expect(bailGroups).toEqual([
|
||||
[ [,, 'required'] ], // bail on this one
|
||||
[ [,, 'max'] ], // bail on this one
|
||||
[ [,, 'matches'] ], // bail on this one
|
||||
[ [,, 'min'] ] // bail on this one
|
||||
])
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([true, true, true, true])
|
||||
})
|
||||
|
||||
it('splits no rules when bail is at the end', () => {
|
||||
const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'matches'], [,,'min'], [,, 'bail']])
|
||||
expect(bailGroups).toEqual([
|
||||
[ [,, 'required'], [,, 'max'], [,, 'matches'], [,, 'min'] ] // dont bail on these
|
||||
])
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([false])
|
||||
})
|
||||
|
||||
it('splits individual modified names into two groups when at the begining', () => {
|
||||
const bailGroups = groupBails([[,,'required', '^'], [,,'max'], [,, 'matches'], [,,'min'] ])
|
||||
expect(bailGroups).toEqual([
|
||||
[ [,, 'required', '^'] ], // bail on this one
|
||||
[ [,, 'max'], [,, 'matches'], [,, 'min'] ] // dont bail on these
|
||||
])
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([true, false])
|
||||
})
|
||||
|
||||
it('splits individual modified names into three groups when in the middle', () => {
|
||||
const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'matches', '^'], [,,'min'] ])
|
||||
expect(bailGroups).toEqual([
|
||||
[ [,, 'required'], [,, 'max'] ], // dont bail on these
|
||||
[ [,, 'matches', '^'] ], // bail on this one
|
||||
[ [,, 'min'] ] // dont bail on this
|
||||
])
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([false, true, false])
|
||||
})
|
||||
|
||||
it('splits individual modified names into four groups when used twice', () => {
|
||||
const bailGroups = groupBails([[,,'required', '^'], [,,'max'], [,, 'matches', '^'], [,,'min'] ])
|
||||
expect(bailGroups).toEqual([
|
||||
[ [,, 'required', '^'] ], // bail on this
|
||||
[ [,, 'max'] ], // dont bail on this
|
||||
[ [,, 'matches', '^'] ], // bail on this
|
||||
[ [,, 'min'] ] // dont bail on this
|
||||
])
|
||||
expect(bailGroups.map(group => !!group.bail)).toEqual([true, false, true, false])
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import rules from '@/validation/rules.ts'
|
||||
import FileUpload from '../../src/FileUpload'
|
||||
|
||||
|
||||
/**
|
||||
@ -320,32 +319,6 @@ describe('matches', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Mime types.
|
||||
*/
|
||||
describe('mime', () => {
|
||||
it('passes basic image/jpeg stack', async () => {
|
||||
const fileUpload = new FileUpload({
|
||||
files: [ { type: 'image/jpeg' } ]
|
||||
})
|
||||
expect(await rules.mime({ value: fileUpload }, 'image/png', 'image/jpeg')).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when match is at begining of stack', async () => {
|
||||
const fileUpload = new FileUpload({
|
||||
files: [ { type: 'document/pdf' } ]
|
||||
})
|
||||
expect(await rules.mime({ value: fileUpload }, 'document/pdf')).toBe(true)
|
||||
})
|
||||
|
||||
it('fails when not in stack', async () => {
|
||||
const fileUpload = new FileUpload({
|
||||
files: [ { type: 'application/json' } ]
|
||||
})
|
||||
expect(await rules.mime({ value: fileUpload }, 'image/png', 'image/jpeg')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Minimum.
|
||||
*/
|
||||
@ -459,10 +432,6 @@ describe('required', () => {
|
||||
it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true))
|
||||
|
||||
it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true))
|
||||
|
||||
it('passes with FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [{ name: 'j.png' }] }) })).toBe(true))
|
||||
|
||||
it('fails with empty FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [] }) })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
36
test/unit/validation/validator.test.js
Normal file
36
test/unit/validation/validator.test.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { enlarge } from '@/validation/validator.ts'
|
||||
|
||||
// @TODO: Converting raw rule data to validator
|
||||
|
||||
describe('Validator', () => {
|
||||
it ('Enlarges validator groups', () => {
|
||||
expect(enlarge([{
|
||||
validators: [],
|
||||
bail: false,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: false,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: false,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: true,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: false,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: false,
|
||||
}])).toEqual([{
|
||||
validators: [],
|
||||
bail: false,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: true,
|
||||
}, {
|
||||
validators: [],
|
||||
bail: false,
|
||||
}])
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user