Merge pull request #9 from cmath10/proposal-cleanup
Validation logic refactor, dead code removal, plugin system removed, file upload functionality removed
This commit is contained in:
commit
e1a10ee61e
@ -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,48 +0,0 @@
|
|||||||
import { cloneDeep } from './libs/utils'
|
|
||||||
import FileUpload from './FileUpload'
|
|
||||||
import FormularioForm from '@/FormularioForm.vue'
|
|
||||||
|
|
||||||
export default class FormSubmission {
|
|
||||||
public form: FormularioForm
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a formulario form.
|
|
||||||
* @param {vm} form an instance of FormularioForm
|
|
||||||
*/
|
|
||||||
constructor (form: FormularioForm) {
|
|
||||||
this.form = form
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the form has any validation errors.
|
|
||||||
*/
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
|
||||||
return (this.form as any).hasValidationErrors()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously generate the values payload of this form.
|
|
||||||
*/
|
|
||||||
values (): Promise<Record<string, any>> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const form = this.form as any
|
|
||||||
const pending = []
|
|
||||||
const values = cloneDeep(form.proxy)
|
|
||||||
|
|
||||||
for (const key in values) {
|
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(values, key) &&
|
|
||||||
typeof form.proxy[key] === 'object' &&
|
|
||||||
form.proxy[key] instanceof FileUpload
|
|
||||||
) {
|
|
||||||
pending.push(
|
|
||||||
form.proxy[key].upload().then((data: Record<string, any>) => Object.assign(values, { [key]: data }))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Promise.all(pending)
|
|
||||||
.then(() => resolve(values))
|
|
||||||
.catch(err => reject(err))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +1,22 @@
|
|||||||
import { VueConstructor } from 'vue'
|
import { VueConstructor } from 'vue'
|
||||||
|
|
||||||
import library from '@/libs/library'
|
import { has } from '@/libs/utils'
|
||||||
import rules from '@/validation/rules'
|
import rules from '@/validation/rules'
|
||||||
import mimes from '@/libs/mimes'
|
import messages from '@/validation/messages'
|
||||||
import FileUpload from '@/FileUpload'
|
|
||||||
import RuleValidationMessages from '@/RuleValidationMessages'
|
|
||||||
import { arrayify, has } from '@/libs/utils'
|
|
||||||
import fauxUploader from '@/libs/faux-uploader'
|
|
||||||
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 FormularioInput from '@/FormularioInput.vue'
|
||||||
import FormularioGrouping from '@/FormularioGrouping.vue'
|
import FormularioGrouping from '@/FormularioGrouping.vue'
|
||||||
import { ValidationContext } from '@/validation/types'
|
|
||||||
|
|
||||||
interface ErrorHandler {
|
import {
|
||||||
(error: any, formName?: string): any;
|
ValidationContext,
|
||||||
}
|
ValidationRule,
|
||||||
|
} from '@/validation/types'
|
||||||
|
|
||||||
interface FormularioOptions {
|
interface FormularioOptions {
|
||||||
components?: { [name: string]: VueConstructor };
|
|
||||||
plugins?: any[];
|
|
||||||
library?: any;
|
|
||||||
rules?: any;
|
rules?: any;
|
||||||
mimes?: any;
|
|
||||||
locale?: any;
|
|
||||||
uploader?: any;
|
|
||||||
uploadUrl?: any;
|
|
||||||
fileUrlKey?: any;
|
|
||||||
errorHandler?: ErrorHandler;
|
|
||||||
uploadJustCompleteDuration?: any;
|
|
||||||
validationMessages?: any;
|
validationMessages?: any;
|
||||||
idPrefix?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
@ -40,55 +25,26 @@ interface FormularioOptions {
|
|||||||
*/
|
*/
|
||||||
export default class Formulario {
|
export default class Formulario {
|
||||||
public options: FormularioOptions
|
public options: FormularioOptions
|
||||||
public defaults: FormularioOptions
|
|
||||||
public registry: Map<string, FormularioForm>
|
|
||||||
public idRegistry: { [name: string]: number }
|
public idRegistry: { [name: string]: number }
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate our base options.
|
|
||||||
*/
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.options = {}
|
this.options = {
|
||||||
this.defaults = {
|
|
||||||
components: {
|
|
||||||
FormularioForm,
|
|
||||||
FormularioInput,
|
|
||||||
FormularioGrouping,
|
|
||||||
},
|
|
||||||
library,
|
|
||||||
rules,
|
rules,
|
||||||
mimes,
|
validationMessages: messages,
|
||||||
locale: false,
|
|
||||||
uploader: fauxUploader,
|
|
||||||
uploadUrl: false,
|
|
||||||
fileUrlKey: 'url',
|
|
||||||
uploadJustCompleteDuration: 1000,
|
|
||||||
errorHandler: (error: any) => error,
|
|
||||||
plugins: [RuleValidationMessages],
|
|
||||||
validationMessages: {},
|
|
||||||
idPrefix: 'formulario-'
|
|
||||||
}
|
}
|
||||||
this.registry = new Map()
|
|
||||||
this.idRegistry = {}
|
this.idRegistry = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install vue formulario, and register it’s components.
|
* Install vue formulario, and register it’s components.
|
||||||
*/
|
*/
|
||||||
install (Vue: VueConstructor, options?: FormularioOptions) {
|
install (Vue: VueConstructor, options?: FormularioOptions): void {
|
||||||
Vue.prototype.$formulario = this
|
Vue.prototype.$formulario = this
|
||||||
this.options = this.defaults
|
Vue.component('FormularioForm', FormularioForm)
|
||||||
let plugins = this.defaults.plugins as any[]
|
Vue.component('FormularioGrouping', FormularioGrouping)
|
||||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
Vue.component('FormularioInput', FormularioInput)
|
||||||
plugins = plugins.concat(options.plugins)
|
|
||||||
}
|
|
||||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
|
||||||
this.extend(options || {})
|
this.extend(options || {})
|
||||||
for (const componentName in this.options.components) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this.options.components, componentName)) {
|
|
||||||
Vue.component(componentName, this.options.components[componentName])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,22 +53,21 @@ export default class Formulario {
|
|||||||
* However, SSR and deterministic ids can be very challenging, so this
|
* However, SSR and deterministic ids can be very challenging, so this
|
||||||
* implementation is open to community review.
|
* implementation is open to community review.
|
||||||
*/
|
*/
|
||||||
nextId (vm: Vue) {
|
nextId (vm: Vue): string {
|
||||||
const options = this.options as FormularioOptions
|
|
||||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
||||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
||||||
if (!has(this.idRegistry, pathPrefix)) {
|
if (!has(this.idRegistry, pathPrefix)) {
|
||||||
this.idRegistry[pathPrefix] = 0
|
this.idRegistry[pathPrefix] = 0
|
||||||
}
|
}
|
||||||
return `${options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
return `formulario-${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a set of options, apply them to the pre-existing options.
|
* Given a set of options, apply them to the pre-existing options.
|
||||||
*/
|
*/
|
||||||
extend (extendWith: FormularioOptions) {
|
extend (extendWith: FormularioOptions): Formulario {
|
||||||
if (typeof extendWith === 'object') {
|
if (typeof extendWith === 'object') {
|
||||||
this.options = merge(this.options as FormularioOptions, extendWith)
|
this.options = merge(this.options, extendWith)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
||||||
@ -121,124 +76,18 @@ export default class Formulario {
|
|||||||
/**
|
/**
|
||||||
* Get validation rules by merging any passed in with global rules.
|
* Get validation rules by merging any passed in with global rules.
|
||||||
*/
|
*/
|
||||||
rules (rules: Record<string, any> = {}) {
|
rules (rules: Record<string, ValidationRule> = {}): () => Record<string, ValidationRule> {
|
||||||
return { ...this.options.rules, ...rules }
|
return { ...this.options.rules, ...rules }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation message for a particular error.
|
* Get the validation message for a particular error.
|
||||||
*/
|
*/
|
||||||
validationMessage (rule: string, context: ValidationContext, vm: Vue) {
|
validationMessage (rule: string, context: ValidationContext, vm: Vue): string {
|
||||||
if (has(this.options.validationMessages, rule)) {
|
if (has(this.options.validationMessages, rule)) {
|
||||||
return this.options.validationMessages[rule](vm, context)
|
return this.options.validationMessages[rule](vm, context)
|
||||||
} else {
|
} else {
|
||||||
return this.options.validationMessages.default(vm, context)
|
return this.options.validationMessages.default(vm, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an instance of a FormularioForm register it.
|
|
||||||
*/
|
|
||||||
register (form: FormularioForm) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (form.$options.name === 'FormularioForm' && form.name) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.registry.set(form.name, form)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an instance of a form, remove it from the registry.
|
|
||||||
*/
|
|
||||||
deregister (form: FormularioForm) {
|
|
||||||
if (
|
|
||||||
form.$options.name === 'FormularioForm' &&
|
|
||||||
// @ts-ignore
|
|
||||||
form.name &&
|
|
||||||
// @ts-ignore
|
|
||||||
this.registry.has(form.name as string)
|
|
||||||
) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.registry.delete(form.name as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an array, this function will attempt to make sense of the given error
|
|
||||||
* and hydrate a form with the resulting errors.
|
|
||||||
*/
|
|
||||||
handle (error: any, formName: string, skip = false) {
|
|
||||||
// @ts-ignore
|
|
||||||
const e = skip ? error : this.options.errorHandler(error, formName)
|
|
||||||
if (formName && this.registry.has(formName)) {
|
|
||||||
const form = this.registry.get(formName) as FormularioForm
|
|
||||||
// @ts-ignore
|
|
||||||
form.applyErrors({
|
|
||||||
formErrors: arrayify(e.formErrors),
|
|
||||||
inputErrors: e.inputErrors || {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset a form.
|
|
||||||
*/
|
|
||||||
reset (formName: string, initialValue: Record<string, any> = {}) {
|
|
||||||
this.resetValidation(formName)
|
|
||||||
this.setValues(formName, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the form's validation messages.
|
|
||||||
*/
|
|
||||||
resetValidation (formName: string) {
|
|
||||||
const form = this.registry.get(formName) as FormularioForm
|
|
||||||
// @ts-ignore
|
|
||||||
form.hideErrors(formName)
|
|
||||||
// @ts-ignore
|
|
||||||
form.namedErrors = []
|
|
||||||
// @ts-ignore
|
|
||||||
form.namedFieldErrors = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the form values.
|
|
||||||
*/
|
|
||||||
setValues (formName: string, values?: Record<string, any>) {
|
|
||||||
if (values) {
|
|
||||||
const form = this.registry.get(formName) as FormularioForm
|
|
||||||
// @ts-ignore
|
|
||||||
form.setValues({ ...values })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file uploader.
|
|
||||||
*/
|
|
||||||
getUploader () {
|
|
||||||
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 () {
|
|
||||||
return this.options.fileUrlKey || 'url'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance of an upload.
|
|
||||||
*/
|
|
||||||
createUpload (data: DataTransfer, context: Record<string, any>) {
|
|
||||||
return new FileUpload(data, context, this.options)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ul
|
|
||||||
v-if="fileUploads.length"
|
|
||||||
class="formulario-files"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="file in fileUploads"
|
|
||||||
:key="file.uuid"
|
|
||||||
:data-has-error="!!file.error"
|
|
||||||
:data-has-preview="!!(imagePreview && file.previewData)"
|
|
||||||
>
|
|
||||||
<div class="formulario-file">
|
|
||||||
<div
|
|
||||||
v-if="!!(imagePreview && file.previewData)"
|
|
||||||
class="formulario-file-image-preview"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="file.previewData"
|
|
||||||
alt=""
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="formulario-file-name"
|
|
||||||
:title="file.name"
|
|
||||||
v-text="file.name"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="file.progress !== false"
|
|
||||||
:data-just-finished="file.justFinished"
|
|
||||||
:data-is-finished="!file.justFinished && file.complete"
|
|
||||||
class="formulario-file-progress"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="formulario-file-progress-inner"
|
|
||||||
:style="{width: file.progress + '%'}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="(file.complete && !file.justFinished) || file.progress === false"
|
|
||||||
class="formulario-file-remove"
|
|
||||||
@click="file.removeFile"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="file.error"
|
|
||||||
class="formulario-file-upload-error"
|
|
||||||
v-text="file.error"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import FileUpload from './FileUpload'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FormularioFiles',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
files: {
|
|
||||||
type: FileUpload,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
imagePreview: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
fileUploads () {
|
|
||||||
return this.files.files || []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
files () {
|
|
||||||
if (this.imagePreview) {
|
|
||||||
this.files.loadPreviews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted () {
|
|
||||||
if (this.imagePreview) {
|
|
||||||
this.files.loadPreviews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="formSubmitted">
|
<form @submit.prevent="onFormSubmit">
|
||||||
<slot :errors="mergedFormErrors" />
|
<slot :errors="mergedFormErrors" />
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@ -13,205 +13,173 @@ import {
|
|||||||
Provide,
|
Provide,
|
||||||
Watch,
|
Watch,
|
||||||
} from 'vue-property-decorator'
|
} from 'vue-property-decorator'
|
||||||
import { arrayify, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
import { cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
||||||
import Registry from '@/libs/registry'
|
import merge from '@/utils/merge'
|
||||||
import FormSubmission from '@/FormSubmission'
|
import Registry from '@/form/registry'
|
||||||
import FormularioInput from '@/FormularioInput.vue'
|
import FormularioInput from '@/FormularioInput.vue'
|
||||||
|
|
||||||
import { ErrorObserver } from '@/validation/types'
|
import {
|
||||||
|
ErrorHandler,
|
||||||
|
ErrorObserver,
|
||||||
|
ErrorObserverRegistry,
|
||||||
|
} from '@/validation/ErrorObserver'
|
||||||
|
|
||||||
|
import { ValidationErrorBag } from '@/validation/types'
|
||||||
|
|
||||||
@Component({ name: 'FormularioForm' })
|
@Component({ name: 'FormularioForm' })
|
||||||
export default class FormularioForm extends Vue {
|
export default class FormularioForm extends Vue {
|
||||||
@Provide() formularioFieldValidation (errorObject): void {
|
@Model('input', { default: () => ({}) })
|
||||||
this.$emit('validation', errorObject)
|
public readonly formularioValue!: Record<string, any>
|
||||||
}
|
|
||||||
|
|
||||||
@Provide() formularioRegister = this.register
|
// Errors record, describing state validation errors of whole form
|
||||||
@Provide() formularioDeregister = this.deregister
|
@Prop({ default: () => ({}) }) readonly errors!: Record<string, any>
|
||||||
@Provide() formularioSetter = this.setFieldValue
|
// Form errors only used on FormularioForm default slot
|
||||||
@Provide() getFormValues = (): Record<string, any> => this.proxy
|
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
||||||
@Provide() path = ''
|
|
||||||
|
|
||||||
@Provide() removeErrorObserver (observer): void {
|
@Provide()
|
||||||
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
public 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
|
|
||||||
|
|
||||||
@Prop({ default: false }) readonly errors!: Record<string, any> | boolean
|
|
||||||
@Prop({ default: () => ([]) }) readonly formErrors!: []
|
|
||||||
|
|
||||||
public proxy: Record<string, any> = {}
|
public proxy: Record<string, any> = {}
|
||||||
|
|
||||||
registry: Registry = new Registry(this)
|
registry: Registry = new Registry(this)
|
||||||
|
|
||||||
childrenShouldShowErrors = false
|
private errorObserverRegistry = new ErrorObserverRegistry()
|
||||||
|
// Local error messages are temporal, they wiped each resetValidation call
|
||||||
|
private localFormErrors: string[] = []
|
||||||
|
private localFieldErrors: Record<string, string[]> = {}
|
||||||
|
|
||||||
formShouldShowErrors = false
|
get mergedFormErrors (): string[] {
|
||||||
|
return [...this.formErrors, ...this.localFormErrors]
|
||||||
errorObservers: [] = []
|
|
||||||
|
|
||||||
namedErrors: [] = []
|
|
||||||
|
|
||||||
namedFieldErrors: Record<string, any> = {}
|
|
||||||
|
|
||||||
get mergedFormErrors (): Record<string, any> {
|
|
||||||
return this.formErrors.concat(this.namedErrors)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get mergedFieldErrors (): Record<string, any> {
|
get mergedFieldErrors (): Record<string, string[]> {
|
||||||
const errors = {}
|
return merge(this.errors || {}, this.localFieldErrors)
|
||||||
|
|
||||||
if (this.errors) {
|
|
||||||
for (const fieldName in this.errors) {
|
|
||||||
errors[fieldName] = arrayify(this.errors[fieldName])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fieldName in this.namedFieldErrors) {
|
|
||||||
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasFormErrorObservers (): boolean {
|
get hasModel (): boolean {
|
||||||
return this.errorObservers.some(o => o.type === 'form')
|
return has(this.$options.propsData || {}, 'formularioValue')
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasInitialValue (): boolean {
|
get hasInitialValue (): boolean {
|
||||||
return (
|
return this.formularioValue && typeof this.formularioValue === 'object'
|
||||||
(this.formularioValue && typeof this.formularioValue === 'object') ||
|
|
||||||
(this.values && typeof this.values === 'object') ||
|
|
||||||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get isVmodeled (): boolean {
|
|
||||||
return !!(has(this.$options.propsData, 'formularioValue') &&
|
|
||||||
this._events &&
|
|
||||||
Array.isArray(this._events.input) &&
|
|
||||||
this._events.input.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get initialValues (): Record<string, any> {
|
get initialValues (): Record<string, any> {
|
||||||
if (
|
if (this.hasModel && typeof this.formularioValue === 'object') {
|
||||||
has(this.$options.propsData, 'formularioValue') &&
|
|
||||||
typeof this.formularioValue === 'object'
|
|
||||||
) {
|
|
||||||
// If there is a v-model on the form/group, use those values as first priority
|
// If there is a v-model on the form/group, use those values as first priority
|
||||||
return Object.assign({}, this.formularioValue) // @todo - use a deep clone to detach reference types
|
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
||||||
} else if (
|
|
||||||
has(this.$options.propsData, 'values') &&
|
|
||||||
typeof this.values === 'object'
|
|
||||||
) {
|
|
||||||
// If there are values, use them as secondary priority
|
|
||||||
return Object.assign({}, this.values)
|
|
||||||
} else if (
|
|
||||||
this.isGrouping && typeof this.context.model[this.index] === 'object'
|
|
||||||
) {
|
|
||||||
return this.context.model[this.index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('formularioValue', { deep: true })
|
@Watch('formularioValue', { deep: true })
|
||||||
onFormularioValueChanged (values): void {
|
onFormularioValueChanged (values: Record<string, any>): void {
|
||||||
if (this.isVmodeled && values && typeof values === 'object') {
|
if (this.hasModel && values && typeof values === 'object') {
|
||||||
this.setValues(values)
|
this.setValues(values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('mergedFormErrors')
|
@Watch('mergedFormErrors')
|
||||||
onMergedFormErrorsChanged (errors): void {
|
onMergedFormErrorsChanged (errors: string[]): void {
|
||||||
this.errorObservers
|
this.errorObserverRegistry.filter(o => o.type === 'form').observe(errors)
|
||||||
.filter(o => o.type === 'form')
|
|
||||||
.forEach(o => o.callback(errors))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('mergedFieldErrors', { immediate: true })
|
@Watch('mergedFieldErrors', { immediate: true })
|
||||||
onMergedFieldErrorsChanged (errors): void {
|
onMergedFieldErrorsChanged (errors: Record<string, string[]>): void {
|
||||||
this.errorObservers
|
this.errorObserverRegistry.filter(o => o.type === 'input').observe(errors)
|
||||||
.filter(o => o.type === 'input')
|
|
||||||
.forEach(o => o.callback(errors[o.field] || []))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
created (): void {
|
created (): void {
|
||||||
this.$formulario.register(this)
|
this.initProxy()
|
||||||
this.applyInitialValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyed (): void {
|
|
||||||
this.$formulario.deregister(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
public register (field: string, component: FormularioInput): void {
|
|
||||||
this.registry.register(field, component)
|
|
||||||
}
|
|
||||||
|
|
||||||
public deregister (field: string): void {
|
|
||||||
this.registry.remove(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrors ({ formErrors, inputErrors }): void {
|
|
||||||
// given an object of errors, apply them to this form
|
|
||||||
this.namedErrors = formErrors
|
|
||||||
this.namedFieldErrors = inputErrors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
addErrorObserver (observer: ErrorObserver): void {
|
getFormValues (): Record<string, any> {
|
||||||
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
|
return this.proxy
|
||||||
this.errorObservers.push(observer)
|
|
||||||
if (observer.type === 'form') {
|
|
||||||
observer.callback(this.mergedFormErrors)
|
|
||||||
} else if (has(this.mergedFieldErrors, observer.field)) {
|
|
||||||
observer.callback(this.mergedFieldErrors[observer.field])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerErrorComponent (component): void {
|
onFormSubmit (): Promise<void> {
|
||||||
if (!this.errorComponents.includes(component)) {
|
return this.hasValidationErrors()
|
||||||
this.errorComponents.push(component)
|
.then(hasErrors => hasErrors ? undefined : cloneDeep(this.proxy))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formSubmitted (): Promise<void> {
|
|
||||||
// perform validation here
|
|
||||||
this.showErrors()
|
|
||||||
const submission = new FormSubmission(this)
|
|
||||||
this.$emit('submit-raw', submission)
|
|
||||||
return submission.hasValidationErrors()
|
|
||||||
.then(hasErrors => hasErrors ? undefined : submission.values())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (typeof data !== 'undefined') {
|
if (typeof data !== 'undefined') {
|
||||||
this.$emit('submit', data)
|
this.$emit('submit', data)
|
||||||
return data
|
} else {
|
||||||
|
this.$emit('error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
applyInitialValues (): void {
|
@Provide()
|
||||||
|
onFormularioFieldValidation (errorBag: ValidationErrorBag): void {
|
||||||
|
this.$emit('validation', errorBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
addErrorObserver (observer: ErrorObserver): void {
|
||||||
|
this.errorObserverRegistry.add(observer)
|
||||||
|
if (observer.type === 'form') {
|
||||||
|
observer.callback(this.mergedFormErrors)
|
||||||
|
} else if (observer.field && has(this.mergedFieldErrors, observer.field)) {
|
||||||
|
observer.callback(this.mergedFieldErrors[observer.field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
removeErrorObserver (observer: ErrorHandler): void {
|
||||||
|
this.errorObserverRegistry.remove(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide('formularioRegister')
|
||||||
|
register (field: string, component: FormularioInput): void {
|
||||||
|
this.registry.register(field, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide('formularioDeregister')
|
||||||
|
deregister (field: string): void {
|
||||||
|
this.registry.remove(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
initProxy (): void {
|
||||||
if (this.hasInitialValue) {
|
if (this.hasInitialValue) {
|
||||||
this.proxy = this.initialValues
|
this.proxy = this.initialValues
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFieldValue (field, value, emit: boolean = true): void {
|
setValues (values: Record<string, any>): void {
|
||||||
|
const keys = Array.from(new Set([...Object.keys(values), ...Object.keys(this.proxy)]))
|
||||||
|
let proxyHasChanges = false
|
||||||
|
keys.forEach(field => {
|
||||||
|
if (!this.registry.hasNested(field)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registry.getNested(field).forEach((registryField, registryKey) => {
|
||||||
|
const $input = this.registry.get(registryKey) as FormularioInput
|
||||||
|
const oldValue = getNested(this.proxy, registryKey)
|
||||||
|
const newValue = getNested(values, registryKey)
|
||||||
|
|
||||||
|
if (!shallowEqualObjects(newValue, oldValue)) {
|
||||||
|
this.setFieldValue(registryKey, newValue, false)
|
||||||
|
proxyHasChanges = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shallowEqualObjects(newValue, $input.proxy)) {
|
||||||
|
$input.context.model = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.initProxy()
|
||||||
|
|
||||||
|
if (proxyHasChanges) {
|
||||||
|
this.$emit('input', { ...this.proxy })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide('formularioSetter')
|
||||||
|
setFieldValue (field: string, value: any, emit = true): void {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { [field]: value, ...proxy } = this.proxy
|
const { [field]: value, ...proxy } = this.proxy
|
||||||
@ -219,66 +187,31 @@ export default class FormularioForm extends Vue {
|
|||||||
} else {
|
} else {
|
||||||
setNested(this.proxy, field, value)
|
setNested(this.proxy, field, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emit) {
|
if (emit) {
|
||||||
this.$emit('input', Object.assign({}, this.proxy))
|
this.$emit('input', Object.assign({}, this.proxy))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
hasValidationErrors (): Promise<boolean> {
|
||||||
return Promise.all(this.registry.reduce((resolvers, cmp) => {
|
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
|
||||||
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
|
resolvers.push(input.performValidation() && input.hasValidationErrors())
|
||||||
return resolvers
|
return resolvers
|
||||||
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
|
}, [])).then(results => results.some(hasErrors => hasErrors))
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrors (): void {
|
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
||||||
this.childrenShouldShowErrors = true
|
// 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) => {
|
this.registry.forEach((input: FormularioInput) => {
|
||||||
input.formShouldShowErrors = true
|
input.resetValidation()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hideErrors (): void {
|
|
||||||
this.childrenShouldShowErrors = false
|
|
||||||
this.registry.forEach((input: FormularioInput) => {
|
|
||||||
input.formShouldShowErrors = false
|
|
||||||
input.behavioralErrorVisibility = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setValues (values: Record<string, any>): void {
|
|
||||||
// Collect all keys, existing and incoming
|
|
||||||
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
|
|
||||||
let proxyHasChanges = false;
|
|
||||||
keys.forEach(field => {
|
|
||||||
if (this.registry.hasNested(field)) {
|
|
||||||
this.registry.getNested(field).forEach((registryField, registryKey) => {
|
|
||||||
if (
|
|
||||||
!shallowEqualObjects(
|
|
||||||
getNested(values, registryKey),
|
|
||||||
getNested(this.proxy, registryKey)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.setFieldValue(registryKey, getNested(values, registryKey), false)
|
|
||||||
proxyHasChanges = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!shallowEqualObjects(
|
|
||||||
getNested(values, registryKey),
|
|
||||||
this.registry.get(registryKey).proxy
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.registry.get(registryKey).context.model = getNested(values, registryKey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.applyInitialValues()
|
|
||||||
|
|
||||||
if (proxyHasChanges) {
|
|
||||||
this.$emit('input', Object.assign({}, this.proxy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div>
|
||||||
class="formulario-group"
|
|
||||||
data-type="group"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="formulario-input">
|
||||||
class="formulario-input"
|
|
||||||
:data-has-errors="hasErrors"
|
|
||||||
:data-is-showing-errors="hasVisibleErrors"
|
|
||||||
:data-type="type"
|
|
||||||
>
|
|
||||||
<slot
|
<slot
|
||||||
:id="id"
|
:id="id"
|
||||||
:context="context"
|
:context="context"
|
||||||
:errors="errors"
|
:violations="validationErrors"
|
||||||
:validationErrors="validationErrors"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -21,11 +15,20 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Model,
|
Model,
|
||||||
Prop,
|
Prop,
|
||||||
Provide,
|
|
||||||
Watch,
|
Watch,
|
||||||
} from 'vue-property-decorator'
|
} from 'vue-property-decorator'
|
||||||
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
import { arrayify, has, parseRules, shallowEqualObjects, snakeToCamel } from './libs/utils'
|
||||||
import { ValidationError } from '@/validation/types'
|
import {
|
||||||
|
ValidationContext,
|
||||||
|
ValidationError,
|
||||||
|
ValidationRule,
|
||||||
|
} from '@/validation/types'
|
||||||
|
import {
|
||||||
|
createValidatorGroups,
|
||||||
|
validate,
|
||||||
|
Validator,
|
||||||
|
ValidatorGroup,
|
||||||
|
} from '@/validation/validator'
|
||||||
|
|
||||||
const ERROR_BEHAVIOR = {
|
const ERROR_BEHAVIOR = {
|
||||||
BLUR: 'blur',
|
BLUR: 'blur',
|
||||||
@ -36,7 +39,7 @@ const ERROR_BEHAVIOR = {
|
|||||||
@Component({ name: 'FormularioInput', inheritAttrs: false })
|
@Component({ name: 'FormularioInput', inheritAttrs: false })
|
||||||
export default class FormularioInput extends Vue {
|
export default class FormularioInput extends Vue {
|
||||||
@Inject({ default: undefined }) formularioSetter!: Function|undefined
|
@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 }) formularioRegister!: Function|undefined
|
||||||
@Inject({ default: undefined }) formularioDeregister!: Function|undefined
|
@Inject({ default: undefined }) formularioDeregister!: Function|undefined
|
||||||
@Inject({ default: () => (): Record<string, any> => ({}) }) getFormValues!: Function
|
@Inject({ default: () => (): Record<string, any> => ({}) }) getFormValues!: Function
|
||||||
@ -44,102 +47,46 @@ export default class FormularioInput extends Vue {
|
|||||||
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined
|
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined
|
||||||
@Inject({ default: '' }) path!: string
|
@Inject({ default: '' }) path!: string
|
||||||
|
|
||||||
@Provide() formularioRegisterRule = this.registerRule
|
@Model('input', { default: '' }) formularioValue: any
|
||||||
@Provide() formularioRemoveRule = this.removeRule
|
|
||||||
|
|
||||||
@Model('input', {
|
@Prop({ default: null }) id!: string|number|null
|
||||||
default: '',
|
@Prop({ required: true }) name!: string
|
||||||
}) formularioValue: any
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: [String, Number, Boolean],
|
|
||||||
default: false,
|
|
||||||
}) id!: string|number|boolean
|
|
||||||
|
|
||||||
@Prop({ default: 'text' }) type!: string
|
|
||||||
@Prop({ required: true }) name!: string|boolean
|
|
||||||
@Prop({ default: false }) value!: any
|
@Prop({ default: false }) value!: any
|
||||||
|
@Prop({ default: '' }) validation!: string|any[]
|
||||||
|
@Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRule>
|
||||||
|
@Prop({ default: () => ({}) }) validationMessages!: Record<string, any>
|
||||||
|
@Prop({ default: () => [] }) errors!: string[]
|
||||||
@Prop({
|
@Prop({
|
||||||
type: [String, Boolean, Array],
|
|
||||||
default: false,
|
|
||||||
}) validation!: string|any[]|boolean
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false,
|
|
||||||
}) validationName!: string|boolean
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
}) validationRules!: Record<string, any>
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
}) validationMessages!: Record<string, any>
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: [Array, String, Boolean],
|
|
||||||
default: false,
|
|
||||||
}) errors!: []|string|boolean
|
|
||||||
|
|
||||||
@Prop({
|
|
||||||
type: String,
|
|
||||||
default: ERROR_BEHAVIOR.BLUR,
|
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
|
}) errorBehavior!: string
|
||||||
|
|
||||||
@Prop({ default: false }) showErrors!: boolean
|
|
||||||
@Prop({ default: false }) disableErrors!: 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)
|
defaultId: string = this.$formulario.nextId(this)
|
||||||
localAttributes: Record<string, any> = {}
|
|
||||||
localErrors: ValidationError[] = []
|
|
||||||
proxy: Record<string, any> = this.getInitialValue()
|
proxy: Record<string, any> = this.getInitialValue()
|
||||||
behavioralErrorVisibility: boolean = this.errorBehavior === 'live'
|
localErrors: string[] = []
|
||||||
formShouldShowErrors = false
|
validationErrors: ValidationError[] = []
|
||||||
validationErrors: [] = []
|
|
||||||
pendingValidation: Promise<any> = Promise.resolve()
|
pendingValidation: Promise<any> = Promise.resolve()
|
||||||
// These registries are used for injected messages registrants only (mostly internal).
|
|
||||||
ruleRegistry: [] = []
|
|
||||||
messageRegistry: Record<string, any> = {}
|
|
||||||
|
|
||||||
get context (): Record<string, any> {
|
get context (): Record<string, any> {
|
||||||
return this.defineModel({
|
return Object.defineProperty({
|
||||||
id: this.id || this.defaultId,
|
id: this.id || this.defaultId,
|
||||||
name: this.nameOrFallback,
|
name: this.nameOrFallback,
|
||||||
attributes: this.elementAttributes,
|
|
||||||
blurHandler: this.blurHandler.bind(this),
|
blurHandler: this.blurHandler.bind(this),
|
||||||
disableErrors: this.disableErrors,
|
|
||||||
errors: this.explicitErrors,
|
errors: this.explicitErrors,
|
||||||
allErrors: this.allErrors,
|
allErrors: this.allErrors,
|
||||||
formShouldShowErrors: this.formShouldShowErrors,
|
|
||||||
getValidationErrors: this.getValidationErrors.bind(this),
|
|
||||||
hasGivenName: this.hasGivenName,
|
|
||||||
hasValidationErrors: this.hasValidationErrors.bind(this),
|
|
||||||
imageBehavior: this.imageBehavior,
|
|
||||||
performValidation: this.performValidation.bind(this),
|
performValidation: this.performValidation.bind(this),
|
||||||
preventWindowDrops: this.preventWindowDrops,
|
|
||||||
setErrors: this.setErrors.bind(this),
|
|
||||||
showValidationErrors: this.showValidationErrors,
|
|
||||||
uploadBehavior: this.uploadBehavior,
|
|
||||||
uploadUrl: this.mergedUploadUrl,
|
|
||||||
uploader: this.uploader || this.$formulario.getUploader(),
|
|
||||||
validationErrors: this.validationErrors,
|
validationErrors: this.validationErrors,
|
||||||
value: this.value,
|
value: this.value,
|
||||||
visibleValidationErrors: this.visibleValidationErrors,
|
}, 'model', {
|
||||||
|
get: this.modelGetter.bind(this),
|
||||||
|
set: this.modelSetter.bind(this),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get parsedValidationRules () {
|
get parsedValidationRules (): Record<string, ValidationRule> {
|
||||||
const parsedValidationRules = {}
|
const parsedValidationRules: Record<string, ValidationRule> = {}
|
||||||
Object.keys(this.validationRules).forEach(key => {
|
Object.keys(this.validationRules).forEach(key => {
|
||||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
||||||
})
|
})
|
||||||
@ -147,40 +94,13 @@ export default class FormularioInput extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get messages (): Record<string, any> {
|
get messages (): Record<string, any> {
|
||||||
const messages = {}
|
const messages: Record<string, any> = {}
|
||||||
Object.keys(this.validationMessages).forEach((key) => {
|
Object.keys(this.validationMessages).forEach((key) => {
|
||||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||||
})
|
})
|
||||||
Object.keys(this.messageRegistry).forEach((key) => {
|
|
||||||
messages[snakeToCamel(key)] = this.messageRegistry[key]
|
|
||||||
})
|
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reducer for attributes that will be applied to each core input element.
|
|
||||||
*/
|
|
||||||
get elementAttributes (): Record<string, any> {
|
|
||||||
const attrs = Object.assign({}, this.localAttributes)
|
|
||||||
// pass the ID prop through to the root element
|
|
||||||
if (this.id) {
|
|
||||||
attrs.id = this.id
|
|
||||||
} else {
|
|
||||||
attrs.id = this.defaultId
|
|
||||||
}
|
|
||||||
// pass an explicitly given name prop through to the root element
|
|
||||||
if (this.hasGivenName) {
|
|
||||||
attrs.name = this.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is help text, have this element be described by it.
|
|
||||||
if (this.help) {
|
|
||||||
attrs['aria-describedby'] = `${attrs.id}-help`
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the element’s name, or select a fallback.
|
* Return the element’s name, or select a fallback.
|
||||||
*/
|
*/
|
||||||
@ -188,28 +108,6 @@ export default class FormularioInput extends Vue {
|
|||||||
return this.path !== '' ? `${this.path}.${this.name}` : this.name
|
return this.path !== '' ? `${this.path}.${this.name}` : this.name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if an input has a user-defined name.
|
|
||||||
*/
|
|
||||||
get hasGivenName (): boolean {
|
|
||||||
return typeof this.name !== 'boolean'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The validation label to use.
|
|
||||||
*/
|
|
||||||
get mergedValidationName (): string {
|
|
||||||
return this.validationName || this.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the uploadURL on the input if it exists, otherwise use the uploadURL
|
|
||||||
* that is defined as a plugin option.
|
|
||||||
*/
|
|
||||||
get mergedUploadUrl (): string | boolean {
|
|
||||||
return this.uploadUrl || this.$formulario.getUploadUrl()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does this computed property have errors
|
* Does this computed property have errors
|
||||||
*/
|
*/
|
||||||
@ -217,87 +115,50 @@ export default class FormularioInput extends Vue {
|
|||||||
return this.allErrors.length > 0
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The merged errors computed property.
|
* The merged errors computed property.
|
||||||
* Each error is an object with fields message (translated message), rule (rule name) and context
|
* Each error is an object with fields message (translated message), rule (rule name) and context
|
||||||
*/
|
*/
|
||||||
get allErrors (): ValidationError[] {
|
get allErrors (): ValidationError[] {
|
||||||
return [
|
return [
|
||||||
...this.explicitErrors,
|
...this.explicitErrors.map(message => ({ message })),
|
||||||
...arrayify(this.validationErrors)
|
...arrayify(this.validationErrors)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All of the currently visible validation errors (does not include error handling)
|
* These are errors we that have been explicitly passed to us.
|
||||||
*/
|
*/
|
||||||
get visibleValidationErrors (): ValidationError[] {
|
get explicitErrors (): string[] {
|
||||||
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
|
return [...arrayify(this.errors), ...this.localErrors]
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* These are errors we that have been explicity passed to us.
|
|
||||||
*/
|
|
||||||
get explicitErrors (): ValidationError[] {
|
|
||||||
return [
|
|
||||||
...arrayify(this.errors),
|
|
||||||
...this.localErrors,
|
|
||||||
].map(message => ({ rule: null, context: null, message }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if this formulario element is v-modeled or not.
|
* Determines if this formulario element is v-modeled or not.
|
||||||
*/
|
*/
|
||||||
get isVmodeled (): boolean {
|
get hasModel (): boolean {
|
||||||
return !!(Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formularioValue') &&
|
return has(this.$options.propsData || {}, 'formularioValue')
|
||||||
this._events &&
|
|
||||||
Array.isArray(this._events.input) &&
|
|
||||||
this._events.input.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the field should show it's error (if it has one)
|
|
||||||
*/
|
|
||||||
get showValidationErrors (): boolean {
|
|
||||||
return this.showErrors || this.formShouldShowErrors || this.behavioralErrorVisibility
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('$attrs', { deep: true })
|
|
||||||
onAttrsChanged (value): void {
|
|
||||||
this.updateLocalAttributes(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('proxy')
|
@Watch('proxy')
|
||||||
onProxyChanged (newValue, oldValue): void {
|
onProxyChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void {
|
||||||
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
||||||
this.performValidation()
|
this.performValidation()
|
||||||
} else {
|
} else {
|
||||||
this.validationErrors = []
|
this.validationErrors = []
|
||||||
}
|
}
|
||||||
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
this.context.model = newValue
|
this.context.model = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('formularioValue')
|
@Watch('formularioValue')
|
||||||
onFormularioValueChanged (newValue, oldValue): void {
|
onFormularioValueChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void {
|
||||||
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
this.context.model = newValue
|
this.context.model = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('showValidationErrors', { immediate: true })
|
|
||||||
onShowValidationErrorsChanged (val): void {
|
|
||||||
this.$emit('error-visibility', val)
|
|
||||||
}
|
|
||||||
|
|
||||||
created (): void {
|
created (): void {
|
||||||
this.applyInitialValue()
|
this.applyInitialValue()
|
||||||
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
||||||
@ -306,7 +167,6 @@ export default class FormularioInput extends Vue {
|
|||||||
if (!this.disableErrors && typeof this.addErrorObserver === 'function') {
|
if (!this.disableErrors && typeof this.addErrorObserver === 'function') {
|
||||||
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||||
}
|
}
|
||||||
this.updateLocalAttributes(this.$attrs)
|
|
||||||
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
||||||
this.performValidation()
|
this.performValidation()
|
||||||
}
|
}
|
||||||
@ -322,21 +182,11 @@ export default class FormularioInput extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the model used throughout the existing context.
|
|
||||||
*/
|
|
||||||
defineModel (context): Record<string, any> {
|
|
||||||
return Object.defineProperty(context, 'model', {
|
|
||||||
get: this.modelGetter.bind(this),
|
|
||||||
set: this.modelSetter.bind(this),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value from a model.
|
* Get the value from a model.
|
||||||
*/
|
*/
|
||||||
modelGetter (): any {
|
modelGetter (): any {
|
||||||
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
|
const model = this.hasModel ? 'formularioValue' : 'proxy'
|
||||||
if (this[model] === undefined) {
|
if (this[model] === undefined) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -346,7 +196,7 @@ export default class FormularioInput extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Set the value from a model.
|
* Set the value from a model.
|
||||||
*/
|
*/
|
||||||
modelSetter (value): void {
|
modelSetter (value: any): void {
|
||||||
if (!shallowEqualObjects(value, this.proxy)) {
|
if (!shallowEqualObjects(value, this.proxy)) {
|
||||||
this.proxy = value
|
this.proxy = value
|
||||||
}
|
}
|
||||||
@ -361,8 +211,8 @@ export default class FormularioInput extends Vue {
|
|||||||
*/
|
*/
|
||||||
blurHandler (): void {
|
blurHandler (): void {
|
||||||
this.$emit('blur')
|
this.$emit('blur')
|
||||||
if (this.errorBehavior === 'blur') {
|
if (this.errorBehavior === ERROR_BEHAVIOR.BLUR) {
|
||||||
this.behavioralErrorVisibility = true
|
this.performValidation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,76 +233,72 @@ export default class FormularioInput extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalAttributes (value): void {
|
performValidation (): Promise<void> {
|
||||||
if (!shallowEqualObjects(value, this.localAttributes)) {
|
this.pendingValidation = this.validate().then(errors => {
|
||||||
this.localAttributes = value
|
this.didValidate(errors)
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
performValidation () {
|
|
||||||
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
|
||||||
// Add in ruleRegistry rules. These are added directly via injection from
|
|
||||||
// children and not part of the standard validation rule set.
|
|
||||||
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
|
||||||
this.pendingValidation = this.runRules(rules)
|
|
||||||
.then(messages => this.didValidate(messages))
|
|
||||||
return this.pendingValidation
|
return this.pendingValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules (rules) {
|
applyValidator (validator: Validator): Promise<ValidationError|false> {
|
||||||
const run = ([rule, args, ruleName]) => {
|
return validate(validator, {
|
||||||
let res = rule({
|
value: this.context.model,
|
||||||
value: this.context.model,
|
name: this.context.name,
|
||||||
getFormValues: this.getFormValues.bind(this),
|
getFormValues: this.getFormValues.bind(this),
|
||||||
name: this.context.name
|
}).then(valid => valid ? false : this.getMessageObject(validator.name, validator.args))
|
||||||
}, ...args)
|
}
|
||||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
|
||||||
return res.then(result => result ? false : this.getMessageObject(ruleName, args))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
applyValidatorGroup (group: ValidatorGroup): Promise<ValidationError[]> {
|
||||||
|
return Promise.all(group.validators.map(this.applyValidator))
|
||||||
|
.then(violations => (violations.filter(v => v !== false) as ValidationError[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
validate (): Promise<ValidationError[]> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const resolveGroups = (groups, allMessages = []) => {
|
const resolveGroups = (groups: ValidatorGroup[], all: ValidationError[] = []): void => {
|
||||||
const ruleGroup = groups.shift()
|
if (groups.length) {
|
||||||
if (Array.isArray(ruleGroup) && ruleGroup.length) {
|
const current = groups.shift() as ValidatorGroup
|
||||||
Promise.all(ruleGroup.map(run))
|
|
||||||
.then(messages => messages.filter(m => !!m))
|
this.applyValidatorGroup(current).then(violations => {
|
||||||
.then(messages => {
|
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
||||||
messages = Array.isArray(messages) ? messages : []
|
if ((violations.length === 0 || !current.bail) && groups.length) {
|
||||||
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
return resolveGroups(groups, all.concat(violations))
|
||||||
if ((!messages.length || !ruleGroup.bail) && groups.length) {
|
}
|
||||||
return resolveGroups(groups, allMessages.concat(messages))
|
return resolve(all.concat(violations))
|
||||||
}
|
})
|
||||||
return resolve(allMessages.concat(messages))
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
resolve([])
|
resolve([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolveGroups(groupBails(rules))
|
resolveGroups(createValidatorGroups(
|
||||||
|
parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
||||||
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
didValidate (messages): void {
|
didValidate (violations: ValidationError[]): void {
|
||||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
const validationChanged = !shallowEqualObjects(violations, this.validationErrors)
|
||||||
this.validationErrors = messages
|
this.validationErrors = violations
|
||||||
if (validationChanged) {
|
if (validationChanged) {
|
||||||
const errorObject = this.getErrorObject()
|
const errorBag = {
|
||||||
this.$emit('validation', errorObject)
|
name: this.context.name,
|
||||||
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
|
errors: this.validationErrors,
|
||||||
this.formularioFieldValidation(errorObject)
|
}
|
||||||
|
this.$emit('validation', errorBag)
|
||||||
|
if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') {
|
||||||
|
this.onFormularioFieldValidation(errorBag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMessageObject (ruleName, args) {
|
getMessageObject (ruleName: string | undefined, args: any[]): ValidationError {
|
||||||
const context = {
|
const context = {
|
||||||
args,
|
args,
|
||||||
name: this.mergedValidationName,
|
name: this.name,
|
||||||
value: this.context.model,
|
value: this.context.model,
|
||||||
vm: this,
|
formValues: this.getFormValues(),
|
||||||
formValues: this.getFormValues()
|
|
||||||
}
|
}
|
||||||
const message = this.getMessageFunc(ruleName)(context)
|
const message = this.getMessageFunc(ruleName || '')(context)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rule: ruleName,
|
rule: ruleName,
|
||||||
@ -472,7 +318,7 @@ export default class FormularioInput extends Vue {
|
|||||||
return (): string => this.messages[ruleName]
|
return (): string => this.messages[ruleName]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (context): string => this.$formulario.validationMessage(ruleName, context, this)
|
return (context: ValidationContext): string => this.$formulario.validationMessage(ruleName, context, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
hasValidationErrors (): Promise<boolean> {
|
||||||
@ -483,40 +329,13 @@ export default class FormularioInput extends Vue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidationErrors () {
|
setErrors (errors: string[]): void {
|
||||||
return new Promise(resolve => {
|
|
||||||
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getErrorObject () {
|
|
||||||
return {
|
|
||||||
name: this.context.nameOrFallback || this.context.name,
|
|
||||||
errors: this.validationErrors.filter(s => typeof s === 'object'),
|
|
||||||
hasErrors: !!this.validationErrors.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors (errors): void {
|
|
||||||
this.localErrors = arrayify(errors)
|
this.localErrors = arrayify(errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRule (rule, args, ruleName, message = null): void {
|
resetValidation (): void {
|
||||||
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
|
this.localErrors = []
|
||||||
// These are the raw rule format since they will be used directly.
|
this.validationErrors = []
|
||||||
this.ruleRegistry.push([rule, args, ruleName])
|
|
||||||
if (message !== null) {
|
|
||||||
this.messageRegistry[ruleName] = message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRule (key): void {
|
|
||||||
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
|
||||||
if (ruleIndex >= 0) {
|
|
||||||
this.ruleRegistry.splice(ruleIndex, 1)
|
|
||||||
delete this.messageRegistry[key]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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 FormularioForm from '@/FormularioForm.vue'
|
||||||
import FormularioInput from '@/FormularioInput.vue'
|
import FormularioInput from '@/FormularioInput.vue'
|
||||||
|
|
||||||
@ -115,10 +115,10 @@ export default class Registry {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.registry.set(field, component)
|
this.registry.set(field, component)
|
||||||
const hasVModelValue = has(component.$options.propsData as Record<string, any>, 'formularioValue')
|
const hasModel = has(component.$options.propsData || {}, 'formularioValue')
|
||||||
const hasValue = has(component.$options.propsData as Record<string, any>, 'value')
|
const hasValue = has(component.$options.propsData || {}, 'value')
|
||||||
if (
|
if (
|
||||||
!hasVModelValue &&
|
!hasModel &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.ctx.hasInitialValue &&
|
this.ctx.hasInitialValue &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -129,7 +129,7 @@ export default class Registry {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
component.context.model = getNested(this.ctx.initialValues, field)
|
component.context.model = getNested(this.ctx.initialValues, field)
|
||||||
} else if (
|
} else if (
|
||||||
(hasVModelValue || hasValue) &&
|
(hasModel || hasValue) &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
||||||
) {
|
) {
|
@ -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,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* library.js
|
|
||||||
*
|
|
||||||
* Note: We're shipping front end code here, file size is critical. This file is
|
|
||||||
* overly terse for that reason alone, we wouldn't necessarily recommend this.
|
|
||||||
*/
|
|
||||||
const fi = 'FormularioInput'
|
|
||||||
const add = (classification: string, c?: string) => ({
|
|
||||||
classification,
|
|
||||||
component: fi + (c || (classification[0].toUpperCase() + classification.substr(1)))
|
|
||||||
})
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// === SINGLE LINE TEXT STYLE INPUTS
|
|
||||||
...[
|
|
||||||
'text',
|
|
||||||
'email',
|
|
||||||
'number',
|
|
||||||
'color',
|
|
||||||
'date',
|
|
||||||
'hidden',
|
|
||||||
'month',
|
|
||||||
'password',
|
|
||||||
'search',
|
|
||||||
'tel',
|
|
||||||
'time',
|
|
||||||
'url',
|
|
||||||
'week',
|
|
||||||
'datetime-local'
|
|
||||||
].reduce((lib, type) => ({ ...lib, [type]: add('text') }), {}),
|
|
||||||
|
|
||||||
// === SLIDER INPUTS
|
|
||||||
range: add('slider'),
|
|
||||||
|
|
||||||
// === MULTI LINE TEXT INPUTS
|
|
||||||
textarea: add('textarea', 'TextArea'),
|
|
||||||
|
|
||||||
// === BOX STYLE INPUTS
|
|
||||||
checkbox: add('box'),
|
|
||||||
radio: add('box'),
|
|
||||||
|
|
||||||
// === BUTTON STYLE INPUTS
|
|
||||||
submit: add('button'),
|
|
||||||
button: add('button'),
|
|
||||||
|
|
||||||
// === SELECT STYLE INPUTS
|
|
||||||
select: add('select'),
|
|
||||||
|
|
||||||
// === FILE TYPE
|
|
||||||
file: add('file'),
|
|
||||||
image: add('file'),
|
|
||||||
|
|
||||||
// === GROUP TYPE
|
|
||||||
group: add('group')
|
|
||||||
}
|
|
@ -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 {
|
export function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
||||||
if (objA === objB) {
|
if (objA === objB) {
|
||||||
return true
|
return true
|
||||||
@ -138,81 +121,29 @@ function parseRule (rule: any, rules: Record<string, any>) {
|
|||||||
*/
|
*/
|
||||||
export function parseRules (validation: any[]|string, rules: any): any[] {
|
export function parseRules (validation: any[]|string, rules: any): any[] {
|
||||||
if (typeof validation === 'string') {
|
if (typeof validation === 'string') {
|
||||||
return parseRules(validation.split('|'), rules)
|
return parseRules(validation.split('|').filter(f => f.length), rules)
|
||||||
}
|
}
|
||||||
if (!Array.isArray(validation)) {
|
if (!Array.isArray(validation)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return validation.map(rule => parseRule(rule, rules)).filter(f => !!f)
|
return validation.map(rule => {
|
||||||
}
|
return parseRule(rule, rules)
|
||||||
|
}).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]]
|
|
||||||
* @param {array} rules
|
|
||||||
*/
|
|
||||||
export function groupBails (rules: 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.
|
* Escape a string for use in regular expressions.
|
||||||
* @param {string} string
|
|
||||||
*/
|
*/
|
||||||
export function escapeRegExp (string: string) {
|
export function escapeRegExp (string: string): string {
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a string format (date) return a regex to match against.
|
* Given a string format (date) return a regex to match against.
|
||||||
* @param {string} format
|
|
||||||
*/
|
*/
|
||||||
export function regexForFormat (format: string) {
|
export function regexForFormat (format: string): RegExp {
|
||||||
const escaped = `^${escapeRegExp(format)}$`
|
const escaped = `^${escapeRegExp(format)}$`
|
||||||
const formats = {
|
const formats: Record<string, string> = {
|
||||||
MM: '(0[1-9]|1[012])',
|
MM: '(0[1-9]|1[012])',
|
||||||
M: '([1-9]|1[012])',
|
M: '([1-9]|1[012])',
|
||||||
DD: '([012][1-9]|3[01])',
|
DD: '([012][1-9]|3[01])',
|
||||||
@ -220,8 +151,8 @@ export function regexForFormat (format: string) {
|
|||||||
YYYY: '\\d{4}',
|
YYYY: '\\d{4}',
|
||||||
YY: '\\d{2}'
|
YY: '\\d{2}'
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||||
// @ts-ignore
|
|
||||||
return regex.replace(format, formats[format])
|
return regex.replace(format, formats[format])
|
||||||
}, escaped))
|
}, escaped))
|
||||||
}
|
}
|
||||||
@ -256,7 +187,7 @@ export function cloneDeep (value: any): any {
|
|||||||
|
|
||||||
for (const key in value) {
|
for (const key in value) {
|
||||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||||
if (isScalar(value[key]) || value[key] instanceof FileUpload) {
|
if (isScalar(value[key])) {
|
||||||
copy[key] = value[key]
|
copy[key] = value[key]
|
||||||
} else {
|
} else {
|
||||||
copy[key] = cloneDeep(value[key])
|
copy[key] = cloneDeep(value[key])
|
||||||
|
55
src/validation/ErrorObserver.ts
Normal file
55
src/validation/ErrorObserver.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { has } from '@/libs/utils'
|
||||||
|
|
||||||
|
export interface ErrorHandler {
|
||||||
|
(errors: Record<string, any> | any[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorObserver {
|
||||||
|
callback: ErrorHandler;
|
||||||
|
type: 'form' | 'input';
|
||||||
|
field?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorObserverPredicate {
|
||||||
|
(value: ErrorObserver, index: number, array: ErrorObserver[]): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorObserverRegistry {
|
||||||
|
private observers: ErrorObserver[] = []
|
||||||
|
|
||||||
|
constructor (observers: ErrorObserver[] = []) {
|
||||||
|
this.observers = observers
|
||||||
|
}
|
||||||
|
|
||||||
|
public add (observer: ErrorObserver): void {
|
||||||
|
if (!this.observers.some(o => o.callback === observer.callback)) {
|
||||||
|
this.observers.push(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove (handler: ErrorHandler): void {
|
||||||
|
this.observers = this.observers.filter(o => o.callback !== handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
public filter (predicate: ErrorObserverPredicate): ErrorObserverRegistry {
|
||||||
|
return new ErrorObserverRegistry(this.observers.filter(predicate))
|
||||||
|
}
|
||||||
|
|
||||||
|
public some (predicate: ErrorObserverPredicate): boolean {
|
||||||
|
return this.observers.some(predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
public observe (errors: Record<string, string[]>|string[]): void {
|
||||||
|
this.observers.forEach(observer => {
|
||||||
|
if (observer.type === 'form') {
|
||||||
|
observer.callback(errors)
|
||||||
|
} else if (
|
||||||
|
observer.field &&
|
||||||
|
!Array.isArray(errors) &&
|
||||||
|
has(errors, observer.field)
|
||||||
|
) {
|
||||||
|
observer.callback(errors[observer.field])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import Formulario from '@/Formulario'
|
|
||||||
import FormularioInput from '@/FormularioInput.vue'
|
|
||||||
import { ValidationContext } from '@/validation/types'
|
import { ValidationContext } from '@/validation/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,29 +11,28 @@ import { ValidationContext } from '@/validation/types'
|
|||||||
* args // Array of rule arguments: between:5,10 (args are ['5', '10'])
|
* args // Array of rule arguments: between:5,10 (args are ['5', '10'])
|
||||||
* name: // The validation name to be used
|
* name: // The validation name to be used
|
||||||
* value: // The value of the field (do not mutate!),
|
* value: // The value of the field (do not mutate!),
|
||||||
* vm: the // FormulateInput instance this belongs to,
|
|
||||||
* formValues: // If wrapped in a FormulateForm, the value of other form fields.
|
* formValues: // If wrapped in a FormulateForm, the value of other form fields.
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
const validationMessages = {
|
export default {
|
||||||
/**
|
/**
|
||||||
* The default render method for error messages.
|
* The default render method for error messages.
|
||||||
*/
|
*/
|
||||||
default (vm: FormularioInput, context: ValidationContext): string {
|
default (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.default', context)
|
return vm.$t('validation.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid accepted value.
|
* Valid accepted value.
|
||||||
*/
|
*/
|
||||||
accepted (vm: FormularioInput, context: ValidationContext): string {
|
accepted (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.accepted', context)
|
return vm.$t('validation.accepted', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date is not after.
|
* The date is not after.
|
||||||
*/
|
*/
|
||||||
after (vm: FormularioInput, context: ValidationContext): string {
|
after (vm: Vue, context: ValidationContext): string {
|
||||||
if (Array.isArray(context.args) && context.args.length) {
|
if (Array.isArray(context.args) && context.args.length) {
|
||||||
return vm.$t('validation.after.compare', context)
|
return vm.$t('validation.after.compare', context)
|
||||||
}
|
}
|
||||||
@ -46,21 +43,21 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* The value is not a letter.
|
* The value is not a letter.
|
||||||
*/
|
*/
|
||||||
alpha (vm: FormularioInput, context: Record<string, any>): string {
|
alpha (vm: Vue, context: Record<string, any>): string {
|
||||||
return vm.$t('validation.alpha', context)
|
return vm.$t('validation.alpha', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule: checks if the value is alpha numeric
|
* Rule: checks if the value is alpha numeric
|
||||||
*/
|
*/
|
||||||
alphanumeric (vm: FormularioInput, context: Record<string, any>): string {
|
alphanumeric (vm: Vue, context: Record<string, any>): string {
|
||||||
return vm.$t('validation.alphanumeric', context)
|
return vm.$t('validation.alphanumeric', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date is not before.
|
* The date is not before.
|
||||||
*/
|
*/
|
||||||
before (vm: FormularioInput, context: ValidationContext): string {
|
before (vm: Vue, context: ValidationContext): string {
|
||||||
if (Array.isArray(context.args) && context.args.length) {
|
if (Array.isArray(context.args) && context.args.length) {
|
||||||
return vm.$t('validation.before.compare', context)
|
return vm.$t('validation.before.compare', context)
|
||||||
}
|
}
|
||||||
@ -71,7 +68,7 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* The value is not between two numbers or lengths
|
* The value is not between two numbers or lengths
|
||||||
*/
|
*/
|
||||||
between (vm: FormularioInput, context: ValidationContext): string {
|
between (vm: Vue, context: ValidationContext): string {
|
||||||
const force = Array.isArray(context.args) && context.args[2] ? context.args[2] : false
|
const force = Array.isArray(context.args) && context.args[2] ? context.args[2] : false
|
||||||
|
|
||||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||||
@ -84,14 +81,14 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* The confirmation field does not match
|
* The confirmation field does not match
|
||||||
*/
|
*/
|
||||||
confirm (vm: FormularioInput, context: ValidationContext): string {
|
confirm (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.confirm', context)
|
return vm.$t('validation.confirm', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is not a valid date.
|
* Is not a valid date.
|
||||||
*/
|
*/
|
||||||
date (vm: FormularioInput, context: ValidationContext): string {
|
date (vm: Vue, context: ValidationContext): string {
|
||||||
if (Array.isArray(context.args) && context.args.length) {
|
if (Array.isArray(context.args) && context.args.length) {
|
||||||
return vm.$t('validation.date.format', context)
|
return vm.$t('validation.date.format', context)
|
||||||
}
|
}
|
||||||
@ -102,21 +99,21 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* Is not a valid email address.
|
* Is not a valid email address.
|
||||||
*/
|
*/
|
||||||
email (vm: FormularioInput, context: ValidationContext): string {
|
email (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.email.default', context)
|
return vm.$t('validation.email.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ends with specified value
|
* Ends with specified value
|
||||||
*/
|
*/
|
||||||
endsWith (vm: FormularioInput, context: ValidationContext): string {
|
endsWith (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.endsWith.default', context)
|
return vm.$t('validation.endsWith.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value is an allowed value.
|
* Value is an allowed value.
|
||||||
*/
|
*/
|
||||||
in: function (vm: FormularioInput, context: ValidationContext): string {
|
in: function (vm: Vue, context: ValidationContext): string {
|
||||||
if (typeof context.value === 'string' && context.value) {
|
if (typeof context.value === 'string' && context.value) {
|
||||||
return vm.$t('validation.in.string', context)
|
return vm.$t('validation.in.string', context)
|
||||||
}
|
}
|
||||||
@ -127,14 +124,14 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* Value is not a match.
|
* Value is not a match.
|
||||||
*/
|
*/
|
||||||
matches (vm: FormularioInput, context: ValidationContext): string {
|
matches (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.matches.default', context)
|
return vm.$t('validation.matches.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum value allowed.
|
* The maximum value allowed.
|
||||||
*/
|
*/
|
||||||
max (vm: FormularioInput, context: ValidationContext): string {
|
max (vm: Vue, context: ValidationContext): string {
|
||||||
const maximum = context.args[0] as number
|
const maximum = context.args[0] as number
|
||||||
|
|
||||||
if (Array.isArray(context.value)) {
|
if (Array.isArray(context.value)) {
|
||||||
@ -150,7 +147,7 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* The (field-level) error message for mime errors.
|
* The (field-level) error message for mime errors.
|
||||||
*/
|
*/
|
||||||
mime (vm: FormularioInput, context: ValidationContext): string {
|
mime (vm: Vue, context: ValidationContext): string {
|
||||||
const types = context.args[0]
|
const types = context.args[0]
|
||||||
|
|
||||||
if (types) {
|
if (types) {
|
||||||
@ -163,7 +160,7 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* The maximum value allowed.
|
* The maximum value allowed.
|
||||||
*/
|
*/
|
||||||
min (vm: FormularioInput, context: ValidationContext): string {
|
min (vm: Vue, context: ValidationContext): string {
|
||||||
const minimum = context.args[0] as number
|
const minimum = context.args[0] as number
|
||||||
|
|
||||||
if (Array.isArray(context.value)) {
|
if (Array.isArray(context.value)) {
|
||||||
@ -179,43 +176,35 @@ const validationMessages = {
|
|||||||
/**
|
/**
|
||||||
* The field is not an allowed value
|
* The field is not an allowed value
|
||||||
*/
|
*/
|
||||||
not (vm: FormularioInput, context: Record<string, any>): string {
|
not (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.not.default', context)
|
return vm.$t('validation.not.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The field is not a number
|
* The field is not a number
|
||||||
*/
|
*/
|
||||||
number (vm: FormularioInput, context: Record<string, any>): string {
|
number (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.number.default', context)
|
return vm.$t('validation.number.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required field.
|
* Required field.
|
||||||
*/
|
*/
|
||||||
required (vm: FormularioInput, context: Record<string, any>): string {
|
required (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.required.default', context)
|
return vm.$t('validation.required.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts with specified value
|
* Starts with specified value
|
||||||
*/
|
*/
|
||||||
startsWith (vm: FormularioInput, context: Record<string, any>): string {
|
startsWith (vm: Vue, context: ValidationContext): string {
|
||||||
return vm.$t('validation.startsWith.default', context)
|
return vm.$t('validation.startsWith.default', context)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value is not a url.
|
* Value is not a url.
|
||||||
*/
|
*/
|
||||||
url (vm: FormularioInput, context: Record<string, any>): string {
|
url (vm: Vue, context: Record<string, any>): string {
|
||||||
return vm.$t('validation.url.default', context)
|
return vm.$t('validation.url.default', context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This creates a vue-formulario plugin that can be imported and used on each
|
|
||||||
* project.
|
|
||||||
*/
|
|
||||||
export default function (instance: Formulario): void {
|
|
||||||
instance.extend({ validationMessages })
|
|
||||||
}
|
|
@ -1,17 +1,7 @@
|
|||||||
// @ts-ignore
|
|
||||||
import isUrl from 'is-url'
|
import isUrl from 'is-url'
|
||||||
import FileUpload from '../FileUpload'
|
|
||||||
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
||||||
import { ValidatableData } from '@/validation/types'
|
import { ValidatableData } from '@/validation/types'
|
||||||
|
|
||||||
interface ConfirmValidatableData extends ValidatableData {
|
|
||||||
getFormValues: () => Record<string, any>;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Library of rules
|
|
||||||
*/
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* Rule: the value must be "yes", "on", "1", or true
|
* Rule: the value must be "yes", "on", "1", or true
|
||||||
@ -90,14 +80,13 @@ export default {
|
|||||||
* Confirm that the value of one field is the same as another, mostly used
|
* Confirm that the value of one field is the same as another, mostly used
|
||||||
* for password confirmations.
|
* for password confirmations.
|
||||||
*/
|
*/
|
||||||
confirm ({ value, getFormValues, name }: ConfirmValidatableData, field?: string): Promise<boolean> {
|
confirm ({ value, getFormValues, name }: ValidatableData, field?: string): Promise<boolean> {
|
||||||
return Promise.resolve(((): boolean => {
|
return Promise.resolve(((): boolean => {
|
||||||
const formValues = getFormValues()
|
|
||||||
let confirmationFieldName = field
|
let confirmationFieldName = field
|
||||||
if (!confirmationFieldName) {
|
if (!confirmationFieldName) {
|
||||||
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
||||||
}
|
}
|
||||||
return formValues[confirmationFieldName] === value
|
return getFormValues()[confirmationFieldName] === value
|
||||||
})())
|
})())
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -161,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.
|
* Check the minimum value of a particular.
|
||||||
*/
|
*/
|
||||||
@ -231,7 +205,7 @@ export default {
|
|||||||
* Rule: checks if the value is only alpha numeric
|
* Rule: checks if the value is only alpha numeric
|
||||||
*/
|
*/
|
||||||
number ({ value }: { value: any }): Promise<boolean> {
|
number ({ value }: { value: any }): Promise<boolean> {
|
||||||
return Promise.resolve(!isNaN(Number(value)))
|
return Promise.resolve(String(value).length > 0 && !isNaN(Number(value)))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,9 +219,6 @@ export default {
|
|||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return !!value.length
|
return !!value.length
|
||||||
}
|
}
|
||||||
if (value instanceof FileUpload) {
|
|
||||||
return value.getFiles().length > 0
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return !!value
|
return !!value
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,34 @@
|
|||||||
interface ValidatableData {
|
export interface ValidatableData {
|
||||||
|
// The value of the field (do not mutate!),
|
||||||
value: any;
|
value: any;
|
||||||
}
|
// If wrapped in a FormulateForm, the value of other form fields.
|
||||||
|
getFormValues(): Record<string, any>;
|
||||||
interface ValidationContext {
|
// The validation name to be used
|
||||||
args: any[];
|
|
||||||
name: string;
|
name: string;
|
||||||
value: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidationError {
|
export interface ValidationContext {
|
||||||
|
// The value of the field (do not mutate!),
|
||||||
|
value: any;
|
||||||
|
// If wrapped in a FormulateForm, the value of other form fields.
|
||||||
|
formValues: Record<string, any>;
|
||||||
|
// The validation name to be used
|
||||||
|
name: string;
|
||||||
|
// Array of rule arguments: between:5,10 (args are ['5', '10'])
|
||||||
|
args: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationRule {
|
||||||
|
(context: ValidatableData, ...args: any[]): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationError {
|
||||||
rule?: string;
|
rule?: string;
|
||||||
context?: any;
|
context?: ValidationContext;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ValidatableData }
|
export interface ValidationErrorBag {
|
||||||
export { ValidationContext }
|
name: string;
|
||||||
export { ValidationError }
|
errors: ValidationError[];
|
||||||
|
|
||||||
export interface ErrorObserver {
|
|
||||||
type: string;
|
|
||||||
field: string;
|
|
||||||
callback: Function;
|
|
||||||
}
|
}
|
||||||
|
79
src/validation/validator.ts
Normal file
79
src/validation/validator.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
ValidatableData,
|
||||||
|
ValidationRule,
|
||||||
|
} from '@/validation/types'
|
||||||
|
|
||||||
|
export type Validator = {
|
||||||
|
name?: string;
|
||||||
|
rule: ValidationRule;
|
||||||
|
args: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ValidatorGroup = {
|
||||||
|
validators: Validator[];
|
||||||
|
bail: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
||||||
|
const enlarged: ValidatorGroup[] = []
|
||||||
|
|
||||||
|
if (groups.length) {
|
||||||
|
let current: ValidatorGroup = groups.shift() as ValidatorGroup
|
||||||
|
enlarged.push(current)
|
||||||
|
groups.forEach((group) => {
|
||||||
|
if (!group.bail && group.bail === current.bail) {
|
||||||
|
current.validators.push(...group.validators)
|
||||||
|
} else {
|
||||||
|
current = { ...group }
|
||||||
|
enlarged.push(current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return enlarged
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]]
|
||||||
|
* @param {array} rules
|
||||||
|
*/
|
||||||
|
export function createValidatorGroups (rules: [ValidationRule, any[], string, string|null][]): ValidatorGroup[] {
|
||||||
|
const mapper = ([
|
||||||
|
rule,
|
||||||
|
args,
|
||||||
|
name,
|
||||||
|
modifier
|
||||||
|
]: [ValidationRule, any[], string, any]): ValidatorGroup => ({
|
||||||
|
validators: [{ name, rule, args }],
|
||||||
|
bail: modifier === '^',
|
||||||
|
})
|
||||||
|
|
||||||
|
const groups: ValidatorGroup[] = []
|
||||||
|
|
||||||
|
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||||
|
|
||||||
|
if (bailIndex >= 0) {
|
||||||
|
groups.push(...enlarge(rules.splice(0, bailIndex + 1).slice(0, -1).map(mapper)))
|
||||||
|
groups.push(...rules.map(([rule, args, name]) => ({
|
||||||
|
validators: [{ rule, args, name }],
|
||||||
|
bail: true,
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
groups.push(...enlarge(rules.map(mapper)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate (validator: Validator, data: ValidatableData): Promise<boolean> {
|
||||||
|
return Promise.resolve(validator.rule(data, ...validator.args))
|
||||||
|
}
|
@ -1,12 +1,7 @@
|
|||||||
import Formulario from '@/index.ts'
|
import Formulario from '@/index.ts'
|
||||||
|
|
||||||
describe('Formulario', () => {
|
describe('Formulario', () => {
|
||||||
it('installs on vue instance', () => {
|
it('Installs on vue instance', () => {
|
||||||
const components = [
|
|
||||||
'FormularioForm',
|
|
||||||
'FormularioInput',
|
|
||||||
'FormularioGrouping',
|
|
||||||
]
|
|
||||||
const registry = []
|
const registry = []
|
||||||
function Vue () {}
|
function Vue () {}
|
||||||
Vue.component = function (name, instance) {
|
Vue.component = function (name, instance) {
|
||||||
@ -14,23 +9,10 @@ describe('Formulario', () => {
|
|||||||
}
|
}
|
||||||
Formulario.install(Vue)
|
Formulario.install(Vue)
|
||||||
expect(Vue.prototype.$formulario).toBe(Formulario)
|
expect(Vue.prototype.$formulario).toBe(Formulario)
|
||||||
expect(registry).toEqual(components)
|
expect(registry).toEqual([
|
||||||
})
|
'FormularioForm',
|
||||||
|
'FormularioGrouping',
|
||||||
it('can extend instance in a plugin', () => {
|
'FormularioInput',
|
||||||
function Vue () {}
|
])
|
||||||
Vue.component = function (name, instance) {}
|
|
||||||
const plugin = function (i) {
|
|
||||||
i.extend({
|
|
||||||
rules: {
|
|
||||||
testRule: () => false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Formulario.install(Vue, {
|
|
||||||
plugins: [ plugin ]
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(typeof Vue.prototype.$formulario.options.rules.testRule).toBe('function')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import flushPromises from 'flush-promises'
|
import flushPromises from 'flush-promises'
|
||||||
import FormSubmission from '@/FormSubmission.ts'
|
|
||||||
import Formulario from '@/index.ts'
|
import Formulario from '@/index.ts'
|
||||||
import FormularioForm from '@/FormularioForm.vue'
|
import FormularioForm from '@/FormularioForm.vue'
|
||||||
|
|
||||||
function validationMessages (instance) {
|
|
||||||
instance.extend({
|
|
||||||
validationMessages: {
|
|
||||||
required: () => 'required',
|
|
||||||
'in': () => 'in',
|
|
||||||
min: () => 'min',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.use(Formulario, {
|
Vue.use(Formulario, {
|
||||||
plugins: [validationMessages]
|
validationMessages: {
|
||||||
|
required: () => 'required',
|
||||||
|
'in': () => 'in',
|
||||||
|
min: () => 'min',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('FormularioForm', () => {
|
describe('FormularioForm', () => {
|
||||||
@ -34,351 +27,213 @@ describe('FormularioForm', () => {
|
|||||||
expect(wrapper.find('form div.default-slot-item').exists()).toBe(true)
|
expect(wrapper.find('form div.default-slot-item').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('intercepts submit event', () => {
|
it('Intercepts submit event', () => {
|
||||||
const formSubmitted = jest.fn()
|
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: {
|
slots: {
|
||||||
default: "<button type='submit' />"
|
default: '<button type="submit" />'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const spy = jest.spyOn(wrapper.vm, 'formSubmitted')
|
const spy = jest.spyOn(wrapper.vm, 'onFormSubmit')
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
expect(spy).toHaveBeenCalled()
|
expect(spy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('registers its subcomponents', () => {
|
it('Adds subcomponents to the registry', () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { formularioValue: { testinput: 'has initial value' } },
|
propsData: { formularioValue: {} },
|
||||||
slots: { default: '<FormularioInput type="text" name="subinput1" /><FormularioInput type="checkbox" name="subinput2" />' }
|
slots: {
|
||||||
|
default: `
|
||||||
|
<FormularioInput name="sub1" />
|
||||||
|
<FormularioInput name="sub2" />
|
||||||
|
`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.vm.registry.keys()).toEqual(['subinput1', 'subinput2'])
|
expect(wrapper.vm.registry.keys()).toEqual(['sub1', 'sub2'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deregisters a subcomponents', async () => {
|
it('Removes subcomponents from the registry', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data () {
|
data: () => ({ active: true }),
|
||||||
return {
|
|
||||||
active: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm>
|
<FormularioForm>
|
||||||
<FormularioInput v-if="active" type="text" name="subinput1" />
|
<FormularioInput v-if="active" name="sub1" />
|
||||||
<FormularioInput type="checkbox" name="subinput2" />
|
<FormularioInput name="sub2" />
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findComponent(FormularioForm).vm.registry.keys()).toEqual(['subinput1', 'subinput2'])
|
expect(wrapper.findComponent(FormularioForm).vm.registry.keys()).toEqual(['sub1', 'sub2'])
|
||||||
wrapper.setData({ active: false })
|
wrapper.setData({ active: false })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findComponent(FormularioForm).vm.registry.keys()).toEqual(['subinput2'])
|
expect(wrapper.findComponent(FormularioForm).vm.registry.keys()).toEqual(['sub2'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can set a field’s initial value', async () => {
|
it('Can set a field’s initial value', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { formularioValue: { testinput: 'has initial value' } },
|
propsData: { formularioValue: { test: 'Has initial value' } },
|
||||||
slots: { default: `
|
slots: {
|
||||||
<FormularioInput v-slot="inputProps" validation="required|in:bar" name="testinput" >
|
default: `
|
||||||
<input v-model="inputProps.context.model" type="text">
|
<FormularioInput v-slot="{ context }" validation="required|in:bar" name="test" >
|
||||||
</FormularioInput>
|
<input v-model="context.model" type="text">
|
||||||
` }
|
</FormularioInput>
|
||||||
|
`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('input').element.value).toBe('has initial value')
|
expect(wrapper.find('input').element['value']).toBe('Has initial value')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lets individual fields override form initial value', () => {
|
it('Lets individual fields override form initial value', () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { formularioValue: { testinput: 'has initial value' } },
|
propsData: { formularioValue: { test: 'has initial value' } },
|
||||||
slots: { default: `
|
slots: {
|
||||||
<FormularioInput v-slot="inputProps" formulario-value="123" name="testinput" >
|
default: `
|
||||||
<input v-model="inputProps.context.model" type="text">
|
<FormularioInput v-slot="{ context }" formulario-value="123" name="test" >
|
||||||
</FormularioInput>
|
<input v-model="context.model" type="text">
|
||||||
` }
|
</FormularioInput>
|
||||||
|
`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.find('input').element.value).toBe('123')
|
expect(wrapper.find('input').element['value']).toBe('123')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lets fields set form initial value with value prop', () => {
|
it('Lets fields set form initial value with value prop', () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data () {
|
data: () => ({ values: {} }),
|
||||||
return {
|
|
||||||
formValues: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `<FormularioForm v-model="formValues">
|
|
||||||
<FormularioInput name="name" value="123" />
|
|
||||||
</FormularioForm>`
|
|
||||||
})
|
|
||||||
expect(wrapper.vm.formValues).toEqual({ name: '123' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('receives updates to form model when individual fields are edited', () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
formValues: {
|
|
||||||
testinput: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm v-model="formValues">
|
<FormularioForm v-model="values">
|
||||||
<FormularioInput v-slot="inputProps" name="testinput" >
|
<FormularioInput name="test" value="123" />
|
||||||
<input v-model="inputProps.context.model" type="text">
|
</FormularioForm>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
expect(wrapper.vm['values']).toEqual({ test: '123' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Receives updates to form model when individual fields are edited', () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
data: () => ({ values: { test: '' } }),
|
||||||
|
template: `
|
||||||
|
<FormularioForm v-model="values">
|
||||||
|
<FormularioInput v-slot="{ context }" name="test" >
|
||||||
|
<input v-model="context.model" type="text">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
wrapper.find('input').setValue('edited value')
|
wrapper.find('input').setValue('Edited value')
|
||||||
expect(wrapper.vm.formValues).toEqual({ testinput: 'edited value' })
|
expect(wrapper.vm['values']).toEqual({ test: 'Edited value' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('field data updates when it is type of date', async () => {
|
it('Field data updates when it is type of date', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data () {
|
data: () => ({ formValues: { date: new Date(123) } }),
|
||||||
return {
|
|
||||||
formValues: {
|
|
||||||
testdate: new Date(123),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm v-model="formValues" ref="form">
|
<FormularioForm v-model="formValues" ref="form">
|
||||||
<FormularioInput v-slot="inputProps" name="testdate" >
|
<FormularioInput v-slot="{ context }" name="date" >
|
||||||
<span v-if="inputProps.context.model">{{ inputProps.context.model.getTime() }}</span>
|
<span v-if="context.model">{{ context.model.getTime() }}</span>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toBe('123')
|
expect(wrapper.find('span').text()).toBe('123')
|
||||||
|
|
||||||
wrapper.setData({ formValues: { testdate: new Date(234) } })
|
wrapper.setData({ formValues: { date: new Date(234) } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toBe('234')
|
expect(wrapper.find('span').text()).toBe('234')
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===========================================================================
|
it('Updates initial form values when input contains a populated v-model', async () => {
|
||||||
/**
|
|
||||||
* @todo in vue-test-utils version 1.0.0-beta.29 has some bugs related to
|
|
||||||
* synchronous updating. Some details are here:
|
|
||||||
*
|
|
||||||
* @update this test was re-implemented in version 1.0.0-beta.31 and seems to
|
|
||||||
* be workign now with flushPromises(). Leaving these docs here for now.
|
|
||||||
*
|
|
||||||
* https://github.com/vuejs/vue-test-utils/issues/1130
|
|
||||||
*
|
|
||||||
* This test is being commented out until there is a resolution on this issue,
|
|
||||||
* and instead being replaced with a mock call.
|
|
||||||
*/
|
|
||||||
|
|
||||||
it('updates initial form values when input contains a populated v-model', async () => {
|
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data () {
|
data: () => ({
|
||||||
return {
|
formValues: { test: '' },
|
||||||
formValues: {
|
fieldValue: '123',
|
||||||
testinput: '',
|
}),
|
||||||
},
|
|
||||||
fieldValue: '123'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm v-model="formValues">
|
<FormularioForm v-model="formValues">
|
||||||
<FormularioInput type="text" name="testinput" v-model="fieldValue" />
|
<FormularioInput name="test" v-model="fieldValue" />
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.vm.formValues).toEqual({ testinput: '123' })
|
expect(wrapper.vm['formValues']).toEqual({ test: '123' })
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
// Replacement test for the above test - not quite as good of a test.
|
// Replacement test for the above test - not quite as good of a test.
|
||||||
it('updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
|
it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: {
|
propsData: {
|
||||||
formularioValue: { testinput: '123' }
|
formularioValue: { test: '123' }
|
||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
default: '<FormularioInput type="text" name="testinput" formulario-value="override-data" />'
|
default: '<FormularioInput name="test" formulario-value="override-data" />'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.emitted().input[wrapper.emitted().input.length - 1]).toEqual([{ testinput: 'override-data' }])
|
expect(wrapper.emitted().input[wrapper.emitted().input.length - 1]).toEqual([{ test: 'override-data' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates an inputs value when the form v-model is modified', async () => {
|
it('updates an inputs value when the form v-model is modified', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data () {
|
data: () => ({ formValues: { test: 'abcd' } }),
|
||||||
return {
|
|
||||||
formValues: {
|
|
||||||
testinput: 'abcd',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm v-model="formValues">
|
<FormularioForm v-model="formValues">
|
||||||
<FormularioInput v-slot="inputProps" name="testinput" >
|
<FormularioInput v-slot="{ context }" name="test" >
|
||||||
<input v-model="inputProps.context.model" type="text">
|
<input v-model="context.model" type="text">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
wrapper.vm.formValues = { testinput: '1234' }
|
wrapper.vm.formValues = { test: '1234' }
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('input[type="text"]').element.value).toBe('1234')
|
expect(wrapper.find('input[type="text"]').element['value']).toBe('1234')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits an instance of FormSubmission', async () => {
|
it('Resolves hasValidationErrors to true', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: { default: '<FormularioInput type="text" formulario-value="123" name="testinput" />' }
|
slots: { default: '<FormularioInput name="fieldName" validation="required" />' }
|
||||||
})
|
})
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.emitted('submit-raw')[0][0]).toBeInstanceOf(FormSubmission)
|
|
||||||
|
const emitted = wrapper.emitted()
|
||||||
|
|
||||||
|
expect(emitted['error']).toBeTruthy()
|
||||||
|
expect(emitted['error'].length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resolves hasValidationErrors to true', async () => {
|
it('Resolves submitted form values to an object', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: { default: '<FormularioInput type="text" validation="required" name="testinput" />' }
|
slots: { default: '<FormularioInput name="fieldName" validation="required" value="Justin" />' }
|
||||||
})
|
})
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
const submission = wrapper.emitted('submit-raw')[0][0]
|
|
||||||
expect(await submission.hasValidationErrors()).toBe(true)
|
const emitted = wrapper.emitted()
|
||||||
|
|
||||||
|
expect(emitted['submit']).toBeTruthy()
|
||||||
|
expect(emitted['submit'].length).toBe(1)
|
||||||
|
expect(emitted['submit'][0]).toEqual([{ fieldName: 'Justin' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resolves submitted form values to an object', async () => {
|
it('Receives a form-errors prop and displays it', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: { default: '<FormularioInput type="text" validation="required" name="testinput" value="Justin" />' }
|
propsData: { formErrors: ['first', 'second'] },
|
||||||
})
|
|
||||||
const submission = await wrapper.vm.formSubmitted()
|
|
||||||
expect(submission).toEqual({testinput: '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()
|
await flushPromises()
|
||||||
expect(wrapper.find('input[type="text"]').element.value).toBe('Dave Barnett')
|
expect(wrapper.vm.mergedFormErrors.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('automatically registers with root plugin', async () => {
|
it('Aggregates form-errors prop with form-named errors', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { formularioValue: { box3: [] }, name: 'login' }
|
propsData: { formErrors: ['first', 'second'] }
|
||||||
})
|
})
|
||||||
expect(wrapper.vm.$formulario.registry.has('login')).toBe(true)
|
wrapper.vm.setErrors({ formErrors: ['third'] })
|
||||||
expect(wrapper.vm.$formulario.registry.get('login')).toBe(wrapper.vm)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls custom error handler with error and name', async () => {
|
|
||||||
const mockHandler = jest.fn((err, name) => err);
|
|
||||||
const wrapper = mount({
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<FormularioForm name="login" />
|
|
||||||
<FormularioForm name="register" />
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
wrapper.vm.$formulario.extend({ errorHandler: mockHandler })
|
|
||||||
wrapper.vm.$formulario.handle({ formErrors: ['This is an error message'] }, 'login')
|
|
||||||
expect(mockHandler.mock.calls.length).toBe(1);
|
|
||||||
expect(mockHandler.mock.calls[0]).toEqual([{ formErrors: ['This is an error message'] }, 'login']);
|
|
||||||
})
|
|
||||||
|
|
||||||
it('errors are displayed on correctly named components', async () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<FormularioForm
|
|
||||||
class="formulario-form formulario-form--login"
|
|
||||||
name="login"
|
|
||||||
v-slot="vSlot"
|
|
||||||
>
|
|
||||||
<span v-for="error in vSlot.errors">{{ error }}</span>
|
|
||||||
</FormularioForm>
|
|
||||||
<FormularioForm
|
|
||||||
class="formulario-form formulario-form--register"
|
|
||||||
name="register"
|
|
||||||
v-slot="vSlot"
|
|
||||||
>
|
|
||||||
<span v-for="error in vSlot.errors">{{ error }}</span>
|
|
||||||
</FormularioForm>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
expect(wrapper.vm.$formulario.registry.has('login') && wrapper.vm.$formulario.registry.has('register')).toBe(true)
|
|
||||||
wrapper.vm.$formulario.handle({ formErrors: ['This is an error message'] }, 'login')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.findAll('.formulario-form').length).toBe(2)
|
|
||||||
expect(wrapper.find('.formulario-form--login span').exists()).toBe(true)
|
|
||||||
expect(wrapper.find('.formulario-form--register span').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('errors are displayed on correctly named components', async () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<FormularioForm
|
|
||||||
class="formulario-form formulario-form--login"
|
|
||||||
name="login"
|
|
||||||
v-slot="vSlot"
|
|
||||||
>
|
|
||||||
<span v-for="error in vSlot.errors">{{ error }}</span>
|
|
||||||
</FormularioForm>
|
|
||||||
<FormularioForm
|
|
||||||
class="formulario-form formulario-form--register"
|
|
||||||
name="register"
|
|
||||||
v-slot="vSlot"
|
|
||||||
>
|
|
||||||
<span v-for="error in vSlot.errors">{{ error }}</span>
|
|
||||||
</FormularioForm>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
expect(wrapper.vm.$formulario.registry.has('login') && wrapper.vm.$formulario.registry.has('register')).toBe(true)
|
|
||||||
wrapper.vm.$formulario.handle({ formErrors: ['This is an error message'] }, 'login')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.findAll('.formulario-form').length).toBe(2)
|
|
||||||
expect(wrapper.find('.formulario-form--login span').exists()).toBe(true)
|
|
||||||
expect(wrapper.find('.formulario-form--register span').exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('receives a form-errors prop and displays it', async () => {
|
|
||||||
const wrapper = mount(FormularioForm, {
|
|
||||||
propsData: { name: 'main', formErrors: ['first', 'second'] },
|
|
||||||
scopedSlots: {
|
|
||||||
default: `
|
|
||||||
<div>
|
|
||||||
<span v-for="error in props.formErrors">{{ error }}</span>
|
|
||||||
<FormularioInput name="name" />
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.vm.$formulario.registry.get('main').mergedFormErrors.length).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('it aggregates form-errors prop with form-named errors', async () => {
|
|
||||||
const wrapper = mount(FormularioForm, {
|
|
||||||
propsData: { formErrors: ['first', 'second'], name: 'login' }
|
|
||||||
})
|
|
||||||
wrapper.vm.$formulario.handle({ formErrors: ['third'] }, 'login')
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
let errors = wrapper.vm.$formulario.registry.get('login').mergedFormErrors
|
expect(Object.keys(wrapper.vm.mergedFormErrors).length).toBe(3)
|
||||||
expect(Object.keys(errors).length).toBe(3)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays field errors on inputs with errors prop', async () => {
|
it('displays field errors on inputs with errors prop', async () => {
|
||||||
@ -386,8 +241,8 @@ describe('FormularioForm', () => {
|
|||||||
propsData: { errors: { fieldWithErrors: ['This field has an error'] }},
|
propsData: { errors: { fieldWithErrors: ['This field has an error'] }},
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioInput v-slot="vSlot" name="fieldWithErrors">
|
<FormularioInput v-slot="{ context }" name="fieldWithErrors">
|
||||||
<span v-for="error in vSlot.context.allErrors">{{ error.message }}</span>
|
<span v-for="error in context.allErrors">{{ error.message }}</span>
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -398,166 +253,143 @@ describe('FormularioForm', () => {
|
|||||||
expect(wrapper.find('span').text()).toEqual('This field has an error')
|
expect(wrapper.find('span').text()).toEqual('This field has an error')
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
it('Is able to display multiple errors on multiple elements', async () => {
|
||||||
|
const errors = { inputA: ['first'], inputB: ['first', 'second']}
|
||||||
|
const wrapper = mount(FormularioForm, { propsData: { errors } })
|
||||||
|
|
||||||
it('is able to display multiple errors on multiple elements', async () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
template: `
|
|
||||||
<FormularioForm
|
|
||||||
name="register"
|
|
||||||
:errors="{inputA: ['first', 'second'], inputB: 'only one here', inputC: ['and one here']}"
|
|
||||||
>
|
|
||||||
<FormularioInput name="inputA" />
|
|
||||||
<FormularioInput name="inputB" type="textarea" />
|
|
||||||
<FormularioInput name="inputC" type="checkbox" />
|
|
||||||
</FormularioForm>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
let errors = wrapper.vm.$formulario.registry.get('register').mergedFieldErrors
|
expect(Object.keys(wrapper.vm.mergedFieldErrors).length).toBe(2)
|
||||||
expect(Object.keys(errors).length).toBe(3)
|
expect(wrapper.vm.mergedFieldErrors.inputA.length).toBe(1)
|
||||||
expect(errors.inputA.length).toBe(2)
|
expect(wrapper.vm.mergedFieldErrors.inputB.length).toBe(2)
|
||||||
expect(errors.inputB.length).toBe(1)
|
|
||||||
expect(errors.inputC.length).toBe(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('it can set multiple field errors with handle()', async () => {
|
it('Can set multiple field errors with setErrors()', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount(FormularioForm)
|
||||||
template: `
|
|
||||||
<FormularioForm name="register">
|
|
||||||
<FormularioInput name="inputA" />
|
|
||||||
<FormularioInput name="inputB" type="textarea" />
|
|
||||||
<FormularioInput name="inputC" type="checkbox" />
|
|
||||||
</FormularioForm>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
let errors = wrapper.vm.$formulario.registry.get('register').mergedFieldErrors
|
expect(Object.keys(wrapper.vm.mergedFieldErrors).length).toBe(0)
|
||||||
expect(Object.keys(errors).length).toBe(0)
|
|
||||||
|
|
||||||
wrapper.vm.$formulario.handle({ inputErrors: {inputA: ['first', 'second'], inputB: 'only one here', inputC: ['and one here']} }, "register")
|
wrapper.vm.setErrors({
|
||||||
await wrapper.vm.$nextTick()
|
inputErrors: {
|
||||||
await flushPromises()
|
inputA: ['first'],
|
||||||
|
inputB: ['first', 'second'],
|
||||||
errors = wrapper.vm.$formulario.registry.get('register').mergedFieldErrors
|
|
||||||
expect(Object.keys(errors).length).toBe(3)
|
|
||||||
expect(errors.inputA.length).toBe(2)
|
|
||||||
expect(errors.inputB.length).toBe(1)
|
|
||||||
expect(errors.inputC.length).toBe(1)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits correct validation event on entry', async () => {
|
|
||||||
const wrapper = mount(FormularioForm, {
|
|
||||||
slots: { default: `
|
|
||||||
<div>
|
|
||||||
<FormularioInput v-slot="inputProps" validation="required|in:bar" name="testinput" >
|
|
||||||
<input v-model="inputProps.context.model" type="text">
|
|
||||||
</FormularioInput>
|
|
||||||
<FormularioInput type="radio" validation="required" name="bar" />
|
|
||||||
</div>
|
|
||||||
` }
|
|
||||||
})
|
|
||||||
wrapper.find('input[type="text"]').setValue('foo')
|
|
||||||
await flushPromises()
|
|
||||||
const errorObjects = wrapper.emitted('validation')
|
|
||||||
// There should be 3 events, both inputs mounting, and the value being set removing required on testinput
|
|
||||||
expect(errorObjects.length).toBe(3)
|
|
||||||
// this should be the event from the setValue()
|
|
||||||
const errorObject = errorObjects[2][0]
|
|
||||||
expect(errorObject).toEqual({
|
|
||||||
name: 'testinput',
|
|
||||||
errors: [
|
|
||||||
expect.any(String)
|
|
||||||
],
|
|
||||||
hasErrors: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits correct validation event when no errors', async () => {
|
|
||||||
const wrapper = mount(FormularioForm, {
|
|
||||||
slots: { default: `
|
|
||||||
<div>
|
|
||||||
<FormularioInput v-slot="inputProps" validation="required|in:bar" name="testinput" >
|
|
||||||
<input v-model="inputProps.context.model" type="text">
|
|
||||||
</FormularioInput>
|
|
||||||
<FormularioInput type="radio" validation="required" name="bar" />
|
|
||||||
</div>
|
|
||||||
` }
|
|
||||||
})
|
|
||||||
wrapper.find('input[type="text"]').setValue('bar')
|
|
||||||
await flushPromises()
|
|
||||||
const errorObjects = wrapper.emitted('validation')
|
|
||||||
expect(errorObjects.length).toBe(3)
|
|
||||||
const errorObject = errorObjects[2][0]
|
|
||||||
expect(errorObject).toEqual({
|
|
||||||
name: 'testinput',
|
|
||||||
errors: [],
|
|
||||||
hasErrors: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes field data when that field is de-registered', async () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
template: `
|
|
||||||
<FormularioForm
|
|
||||||
v-model="formData"
|
|
||||||
>
|
|
||||||
<FormularioInput v-slot="inputProps" name="foo">
|
|
||||||
<input v-model="inputProps.context.model" type="text" value="abc123">
|
|
||||||
</FormularioInput>
|
|
||||||
<FormularioInput type="checkbox" name="bar" v-if="formData.foo !== 'bar'" :value="1" />
|
|
||||||
</FormularioForm>
|
|
||||||
`,
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
formData: {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
wrapper.find('input[type="text"]').setValue('bar')
|
|
||||||
await flushPromises()
|
expect(Object.keys(wrapper.vm.mergedFieldErrors).length).toBe(2)
|
||||||
expect(wrapper.findComponent(FormularioForm).vm.proxy).toEqual({ foo: 'bar' })
|
expect(wrapper.vm.mergedFieldErrors.inputA.length).toBe(1)
|
||||||
expect(wrapper.vm.formData).toEqual({ foo: 'bar' })
|
expect(wrapper.vm.mergedFieldErrors.inputB.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('it allows resetting a form, hiding validation and clearing inputs.', async () => {
|
it('Emits correct validation event when no errors', async () => {
|
||||||
|
const wrapper = mount(FormularioForm, {
|
||||||
|
slots: {
|
||||||
|
default: `
|
||||||
|
<FormularioInput v-slot="{ context }" name="foo" validation="required|in:foo">
|
||||||
|
<input v-model="context.model" type="text" @blur="context.blurHandler">
|
||||||
|
</FormularioInput>
|
||||||
|
<FormularioInput name="bar" validation="required" />
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wrapper.find('input[type="text"]').setValue('foo')
|
||||||
|
wrapper.find('input[type="text"]').trigger('blur')
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.emitted('validation')).toBeTruthy()
|
||||||
|
expect(wrapper.emitted('validation').length).toBe(1)
|
||||||
|
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
||||||
|
name: 'foo',
|
||||||
|
errors: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Emits correct validation event on entry', async () => {
|
||||||
|
const wrapper = mount(FormularioForm, {
|
||||||
|
slots: { default: `
|
||||||
|
<FormularioInput v-slot="{ context }" name="foo" validation="required|in:foo">
|
||||||
|
<input v-model="context.model" type="text" @blur="context.blurHandler">
|
||||||
|
</FormularioInput>
|
||||||
|
<FormularioInput name="bar" validation="required" />
|
||||||
|
` }
|
||||||
|
})
|
||||||
|
wrapper.find('input[type="text"]').setValue('bar')
|
||||||
|
wrapper.find('input[type="text"]').trigger('blur')
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.emitted('validation')).toBeTruthy()
|
||||||
|
expect(wrapper.emitted('validation').length).toBe(1)
|
||||||
|
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
||||||
|
name: 'foo',
|
||||||
|
errors: [ expect.any(Object) ], // @TODO: Check object structure
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
it('Removes field data when that field is de-registered', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
|
data: () => ({ values: {} }),
|
||||||
|
template: `
|
||||||
|
<FormularioForm v-model="values">
|
||||||
|
<FormularioInput v-slot="{ context }" name="foo">
|
||||||
|
<input v-model="context.model" type="text" value="abc123">
|
||||||
|
</FormularioInput>
|
||||||
|
<FormularioInput v-if="values.foo !== 'bar'" name="bar" value="1" />
|
||||||
|
</FormularioForm>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
wrapper.find('input[type="text"]').setValue('bar')
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.findComponent(FormularioForm).vm.proxy).toEqual({ foo: 'bar' })
|
||||||
|
expect(wrapper.vm['values']).toEqual({ foo: 'bar' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows resetting a form, hiding validation and clearing inputs.', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
data: () => ({ values: {} }),
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm
|
<FormularioForm
|
||||||
v-model="formData"
|
v-model="values"
|
||||||
name="login"
|
name="login"
|
||||||
ref="form"
|
ref="form"
|
||||||
>
|
>
|
||||||
<FormularioInput v-slot="inputProps" name="username" validation="required">
|
<FormularioInput v-slot="{ context }" name="username" validation="required">
|
||||||
<input v-model="inputProps.context.model" type="text">
|
<input v-model="context.model" type="text">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
<FormularioInput v-slot="inputProps" name="password" validation="required|min:4,length">
|
<FormularioInput v-slot="{ context }" name="password" validation="required|min:4,length">
|
||||||
<input v-model="inputProps.context.model" type="password">
|
<input v-model="context.model" type="password">
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`,
|
`,
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
formData: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const password = wrapper.find('input[type="password"]')
|
const password = wrapper.find('input[type="password"]')
|
||||||
password.setValue('foo')
|
password.setValue('foo')
|
||||||
password.trigger('blur')
|
password.trigger('blur')
|
||||||
|
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
wrapper.vm.$formulario.handle({
|
wrapper.vm.$refs.form.setErrors({ inputErrors: { username: ['Failed'] } })
|
||||||
inputErrors: { username: ['Failed'] }
|
|
||||||
}, 'login')
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
// First make sure we caugth the errors
|
|
||||||
|
// First make sure we caught the errors
|
||||||
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(1)
|
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(1)
|
||||||
wrapper.vm.$formulario.reset('login')
|
wrapper.vm.$refs.form.resetValidation()
|
||||||
|
wrapper.vm.$refs.form.setValues({ })
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(0)
|
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(0)
|
||||||
expect(wrapper.vm.formData).toEqual({})
|
expect(wrapper.vm.values).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,6 @@ Vue.use(Formulario)
|
|||||||
describe('FormularioGrouping', () => {
|
describe('FormularioGrouping', () => {
|
||||||
it('Grouped fields to be set', async () => {
|
it('Grouped fields to be set', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { name: 'form' },
|
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioGrouping name="group">
|
<FormularioGrouping name="group">
|
||||||
@ -21,21 +20,28 @@ describe('FormularioGrouping', () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.findAll('input[type="text"]').length).toBe(1)
|
|
||||||
wrapper.find('input[type="text"]').setValue('test')
|
|
||||||
|
|
||||||
const submission = await wrapper.vm.formSubmitted()
|
expect(wrapper.findAll('input[type="text"]').length).toBe(1)
|
||||||
expect(submission).toEqual({ group: { text: 'test' } })
|
|
||||||
|
wrapper.find('input[type="text"]').setValue('test')
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const emitted = wrapper.emitted()
|
||||||
|
|
||||||
|
expect(emitted['submit']).toBeTruthy()
|
||||||
|
expect(emitted['submit'].length).toBe(1)
|
||||||
|
expect(emitted['submit'][0]).toEqual([{ group: { text: 'test' } }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Grouped fields to be got', async () => {
|
it('Grouped fields to be got', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: {
|
propsData: {
|
||||||
name: 'form',
|
|
||||||
formularioValue: {
|
formularioValue: {
|
||||||
group: { text: 'Group text' },
|
group: { text: 'Group text' },
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
@ -71,23 +77,20 @@ describe('FormularioGrouping', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Errors are set for grouped fields', async () => {
|
it('Errors are set for grouped fields', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount(FormularioForm, {
|
||||||
data: () => ({ values: {} }),
|
propsData: {
|
||||||
template: `
|
formularioValue: {},
|
||||||
<FormularioForm
|
errors: { 'group.text': 'Test error' },
|
||||||
v-model="values"
|
},
|
||||||
:errors="{'group.text': 'Test error'}"
|
slots: {
|
||||||
name="form"
|
default: `
|
||||||
>
|
|
||||||
<FormularioGrouping name="group">
|
<FormularioGrouping name="group">
|
||||||
<FormularioInput name="text" v-slot="{ context }">
|
<FormularioInput ref="input" name="text" v-slot="{ context }">
|
||||||
<span v-for="error in context.allErrors">
|
<span v-for="error in context.allErrors">{{ error }}</span>
|
||||||
{{ error }}
|
|
||||||
</span>
|
|
||||||
</FormularioInput>
|
</FormularioInput>
|
||||||
</FormularioGrouping>
|
</FormularioGrouping>
|
||||||
</FormularioForm>
|
`,
|
||||||
`
|
},
|
||||||
})
|
})
|
||||||
expect(wrapper.findAll('span').length).toBe(1)
|
expect(wrapper.findAll('span').length).toBe(1)
|
||||||
})
|
})
|
||||||
|
@ -8,20 +8,14 @@ import FormularioInput from '@/FormularioInput.vue'
|
|||||||
|
|
||||||
const globalRule = jest.fn(() => { return false })
|
const globalRule = jest.fn(() => { return false })
|
||||||
|
|
||||||
function validationMessages (instance) {
|
|
||||||
instance.extend({
|
|
||||||
validationMessages: {
|
|
||||||
required: () => 'required',
|
|
||||||
'in': () => 'in',
|
|
||||||
min: () => 'min',
|
|
||||||
globalRule: () => 'globalRule',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.use(Formulario, {
|
Vue.use(Formulario, {
|
||||||
plugins: [validationMessages],
|
rules: { globalRule },
|
||||||
rules: { globalRule }
|
validationMessages: {
|
||||||
|
required: () => 'required',
|
||||||
|
'in': () => 'in',
|
||||||
|
min: () => 'min',
|
||||||
|
globalRule: () => 'globalRule',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('FormularioInput', () => {
|
describe('FormularioInput', () => {
|
||||||
@ -188,74 +182,20 @@ describe('FormularioInput', () => {
|
|||||||
const errorObject = wrapper.emitted('validation')[0][0]
|
const errorObject = wrapper.emitted('validation')[0][0]
|
||||||
expect(errorObject).toEqual({
|
expect(errorObject).toEqual({
|
||||||
name: 'testinput',
|
name: 'testinput',
|
||||||
errors: [
|
errors: [{
|
||||||
{
|
rule: expect.stringContaining('required'),
|
||||||
message: expect.any(String),
|
context: expect.any(Object),
|
||||||
rule: expect.stringContaining('required'),
|
message: expect.any(String),
|
||||||
context: expect.any(Object)
|
}],
|
||||||
}
|
|
||||||
],
|
|
||||||
hasErrors: true
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits a error-visibility event on blur', async () => {
|
it('Can bail on validation when encountering the bail rule', 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 () => {
|
|
||||||
const wrapper = mount(FormularioInput, {
|
const wrapper = mount(FormularioInput, {
|
||||||
propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
|
propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
|
||||||
})
|
})
|
||||||
await flushPromises();
|
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 () => {
|
it('can show multiple validation errors if they occur before the bail rule', async () => {
|
||||||
@ -263,7 +203,7 @@ describe('FormularioInput', () => {
|
|||||||
propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
|
propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
|
||||||
})
|
})
|
||||||
await flushPromises();
|
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 () => {
|
it('can avoid bail behavior by using modifier', async () => {
|
||||||
@ -271,7 +211,7 @@ describe('FormularioInput', () => {
|
|||||||
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' }
|
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' }
|
||||||
})
|
})
|
||||||
await flushPromises();
|
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 () => {
|
it('prevents later error messages when modified rule fails', async () => {
|
||||||
@ -279,7 +219,7 @@ describe('FormularioInput', () => {
|
|||||||
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' }
|
propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' }
|
||||||
})
|
})
|
||||||
await flushPromises();
|
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 () => {
|
it('can bail in the middle of the rule set with a modifier', async () => {
|
||||||
@ -287,7 +227,7 @@ describe('FormularioInput', () => {
|
|||||||
propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' }
|
propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' }
|
||||||
})
|
})
|
||||||
await flushPromises();
|
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 () => {
|
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 rules from '@/validation/rules.ts'
|
||||||
import FileUpload from '@/FileUpload'
|
|
||||||
|
|
||||||
describe('parseRules', () => {
|
describe('parseRules', () => {
|
||||||
it('parses single string rules, returning empty arguments array', () => {
|
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('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||||
|
|
||||||
it('fails on pojo', () => expect(isScalar({})).toBe(false))
|
it('fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||||
|
|
||||||
it('fails on custom type', () => expect(isScalar(FileUpload)).toBe(false))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('cloneDeep', () => {
|
describe('cloneDeep', () => {
|
||||||
@ -175,83 +172,3 @@ describe('snakeToCamel', () => {
|
|||||||
expect(snakeToCamel(fn)).toBe(fn)
|
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 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.
|
* 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', 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 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