diff --git a/src/FileUpload.ts b/src/FileUpload.ts deleted file mode 100644 index d53bd7f..0000000 --- a/src/FileUpload.ts +++ /dev/null @@ -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 - public context: Record - public results: any[] | boolean - - constructor (input: DataTransfer, context: Record = {}, options: Record = {}) { - 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]) { - 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 diff --git a/src/Formulario.ts b/src/Formulario.ts index e2f83a6..2cc564c 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -1,31 +1,22 @@ import { VueConstructor } from 'vue' -import mimes from '@/libs/mimes' import { has } from '@/libs/utils' -import fauxUploader from '@/libs/faux-uploader' import rules from '@/validation/rules' import messages from '@/validation/messages' import merge from '@/utils/merge' -import FileUpload from '@/FileUpload' - import FormularioForm from '@/FormularioForm.vue' import FormularioInput from '@/FormularioInput.vue' import FormularioGrouping from '@/FormularioGrouping.vue' -import { ValidationContext, ValidationRule } from '@/validation/types' +import { + ValidationContext, + ValidationRule, +} from '@/validation/types' interface FormularioOptions { - components?: { [name: string]: VueConstructor }; - plugins?: any[]; rules?: any; - mimes?: any; - uploader?: any; - uploadUrl?: any; - fileUrlKey?: any; - uploadJustCompleteDuration?: any; validationMessages?: any; - idPrefix?: string; } // noinspection JSUnusedGlobalSymbols @@ -38,19 +29,8 @@ export default class Formulario { constructor () { this.options = { - components: { - FormularioForm, - FormularioInput, - FormularioGrouping, - }, rules, - mimes, - uploader: fauxUploader, - uploadUrl: false, - fileUrlKey: 'url', - uploadJustCompleteDuration: 1000, validationMessages: messages, - idPrefix: 'formulario-' } this.idRegistry = {} } @@ -60,12 +40,11 @@ export default class Formulario { */ install (Vue: VueConstructor, options?: FormularioOptions): void { Vue.prototype.$formulario = this + Vue.component('FormularioForm', FormularioForm) + Vue.component('FormularioGrouping', FormularioGrouping) + Vue.component('FormularioInput', FormularioInput) + this.extend(options || {}) - for (const componentName in this.options.components) { - if (has(this.options.components, componentName)) { - Vue.component(componentName, this.options.components[componentName]) - } - } } /** @@ -75,13 +54,12 @@ export default class Formulario { * implementation is open to community review. */ nextId (vm: Vue): string { - const options = this.options as FormularioOptions const path = vm.$route && vm.$route.path ? vm.$route.path : false const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global' if (!has(this.idRegistry, pathPrefix)) { this.idRegistry[pathPrefix] = 0 } - return `${options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}` + return `formulario-${pathPrefix}-${++this.idRegistry[pathPrefix]}` } /** @@ -112,33 +90,4 @@ export default class Formulario { return this.options.validationMessages.default(vm, context) } } - - /** - * Get the file uploader. - */ - getUploader (): any { - return this.options.uploader || false - } - - /** - * Get the global upload url. - */ - getUploadUrl (): string | boolean { - return this.options.uploadUrl || false - } - - /** - * When re-hydrating a file uploader with an array, get the sub-object key to - * access the url of the file. Usually this is just "url". - */ - getFileUrlKey (): string { - return this.options.fileUrlKey || 'url' - } - - /** - * Create a new instance of an upload. - */ - createUpload (data: DataTransfer, context: Record): FileUpload { - return new FileUpload(data, context, this.options) - } } diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue index dc6448f..175f9d3 100644 --- a/src/FormularioForm.vue +++ b/src/FormularioForm.vue @@ -13,8 +13,9 @@ import { Provide, Watch, } from 'vue-property-decorator' -import { arrayify, cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils' -import Registry from '@/libs/registry' +import { cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils' +import merge from '@/utils/merge' +import Registry from '@/form/registry' import FormularioInput from '@/FormularioInput.vue' import { @@ -25,41 +26,21 @@ import { import { ValidationErrorBag } from '@/validation/types' -import FileUpload from '@/FileUpload' - @Component({ name: 'FormularioForm' }) export default class FormularioForm extends Vue { - @Provide() formularioFieldValidation (errorBag: ValidationErrorBag): void { - this.$emit('validation', errorBag) - } - - @Provide() getFormValues = (): Record => this.proxy - @Provide() path = '' - - @Model('input', { - type: Object, - default: () => ({}) - }) readonly formularioValue!: Record - - @Prop({ - type: [String, Boolean], - default: false - }) public readonly name!: string | boolean - - @Prop({ - type: [Object, Boolean], - default: false - }) readonly values!: Record | boolean + @Model('input', { default: () => ({}) }) + public readonly formularioValue!: Record @Prop({ default: () => ({}) }) readonly errors!: Record @Prop({ default: () => ([]) }) readonly formErrors!: string[] + @Provide() + public path = '' + public proxy: Record = {} registry: Registry = new Registry(this) - childrenShouldShowErrors = false - private errorObserverRegistry = new ErrorObserverRegistry() private localFormErrors: string[] = [] private localFieldErrors: Record = {} @@ -68,35 +49,16 @@ export default class FormularioForm extends Vue { return [...this.formErrors, ...this.localFormErrors] } - get mergedFieldErrors (): Record { - const errors: Record = {} - - if (this.errors) { - for (const fieldName in this.errors) { - errors[fieldName] = arrayify(this.errors[fieldName]) - } - } - - for (const fieldName in this.localFieldErrors) { - errors[fieldName] = arrayify(this.localFieldErrors[fieldName]) - } - - return errors - } - - get hasInitialValue (): boolean { - return ( - (this.formularioValue && typeof this.formularioValue === 'object') || - (this.values && typeof this.values === 'object') - ) + get mergedFieldErrors (): Record { + return merge(this.errors || {}, this.localFieldErrors) } get hasModel (): boolean { return has(this.$options.propsData || {}, 'formularioValue') } - get hasValue (): boolean { - return has(this.$options.propsData || {}, 'values') + get hasInitialValue (): boolean { + return this.formularioValue && typeof this.formularioValue === 'object' } get initialValues (): Record { @@ -105,11 +67,6 @@ export default class FormularioForm extends Vue { return { ...this.formularioValue } // @todo - use a deep clone to detach reference types } - if (this.hasValue && typeof this.values === 'object') { - // If there are values, use them as secondary priority - return { ...this.values } - } - return {} } @@ -134,14 +91,14 @@ export default class FormularioForm extends Vue { this.initProxy() } - onFormSubmit (): Promise { - this.childrenShouldShowErrors = true - this.registry.forEach((input: FormularioInput) => { - input.formShouldShowErrors = true - }) + @Provide() + getFormValues (): Record { + return this.proxy + } + onFormSubmit (): Promise { return this.hasValidationErrors() - .then(hasErrors => hasErrors ? undefined : this.getValues()) + .then(hasErrors => hasErrors ? undefined : cloneDeep(this.proxy)) .then(data => { if (typeof data !== 'undefined') { this.$emit('submit', data) @@ -151,6 +108,11 @@ export default class FormularioForm extends Vue { }) } + @Provide() + onFormularioFieldValidation (errorBag: ValidationErrorBag): void { + this.$emit('validation', errorBag) + } + @Provide() addErrorObserver (observer: ErrorObserver): void { this.errorObserverRegistry.add(observer) @@ -176,67 +138,12 @@ export default class FormularioForm extends Vue { this.registry.remove(field) } - resetValidation (): void { - this.localFormErrors = [] - this.localFieldErrors = {} - this.childrenShouldShowErrors = false - this.registry.forEach((input: FormularioInput) => { - input.formShouldShowErrors = false - input.behavioralErrorVisibility = false - }) - } - initProxy (): void { if (this.hasInitialValue) { this.proxy = this.initialValues } } - @Provide('formularioSetter') - setFieldValue (field: string, value: any, emit = true): void { - if (value === undefined) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [field]: value, ...proxy } = this.proxy - this.proxy = proxy - } else { - setNested(this.proxy, field, value) - } - - if (emit) { - this.$emit('input', Object.assign({}, this.proxy)) - } - } - - hasValidationErrors (): Promise { - return Promise.all(this.registry.reduce((resolvers: Promise[], input: FormularioInput) => { - resolvers.push(input.performValidation() && input.hasValidationErrors()) - return resolvers - }, [])).then(results => results.some(hasErrors => hasErrors)) - } - - /** - * Asynchronously generate the values payload of this form. - */ - getValues (): Promise> { - return new Promise((resolve, reject) => { - const pending = [] - const values = cloneDeep(this.proxy) - - for (const key in values) { - if (has(values, key) && typeof this.proxy[key] === 'object' && this.proxy[key] instanceof FileUpload) { - pending.push( - this.proxy[key].upload() - .then((data: Record) => Object.assign(values, { [key]: data })) - ) - } - } - - Promise.all(pending) - .then(() => resolve(values)) - .catch(err => reject(err)) - }) - } - setValues (values: Record): void { const keys = Array.from(new Set([...Object.keys(values), ...Object.keys(this.proxy)])) let proxyHasChanges = false @@ -266,10 +173,40 @@ export default class FormularioForm extends Vue { } } + @Provide('formularioSetter') + setFieldValue (field: string, value: any, emit = true): void { + if (value === undefined) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [field]: value, ...proxy } = this.proxy + this.proxy = proxy + } else { + setNested(this.proxy, field, value) + } + + if (emit) { + this.$emit('input', Object.assign({}, this.proxy)) + } + } + + hasValidationErrors (): Promise { + return Promise.all(this.registry.reduce((resolvers: Promise[], input: FormularioInput) => { + resolvers.push(input.performValidation() && input.hasValidationErrors()) + return resolvers + }, [])).then(results => results.some(hasErrors => hasErrors)) + } + setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record }): void { // given an object of errors, apply them to this form this.localFormErrors = formErrors || [] this.localFieldErrors = inputErrors || {} } + + resetValidation (): void { + this.localFormErrors = [] + this.localFieldErrors = {} + this.registry.forEach((input: FormularioInput) => { + input.resetValidation() + }) + } } diff --git a/src/FormularioGrouping.vue b/src/FormularioGrouping.vue index 2cdacb8..7df7275 100644 --- a/src/FormularioGrouping.vue +++ b/src/FormularioGrouping.vue @@ -1,8 +1,5 @@ diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index ac422a0..73e219a 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -1,15 +1,9 @@ @@ -23,11 +17,10 @@ import { Prop, Watch, } from 'vue-property-decorator' -import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify } from './libs/utils' +import { arrayify, has, parseRules, shallowEqualObjects, snakeToCamel } from './libs/utils' import { ValidationContext, ValidationError, - ValidationErrorBag, ValidationRule, } from '@/validation/types' import { @@ -46,7 +39,7 @@ const ERROR_BEHAVIOR = { @Component({ name: 'FormularioInput', inheritAttrs: false }) export default class FormularioInput extends Vue { @Inject({ default: undefined }) formularioSetter!: Function|undefined - @Inject({ default: () => (): void => {} }) formularioFieldValidation!: Function + @Inject({ default: () => (): void => {} }) onFormularioFieldValidation!: Function @Inject({ default: undefined }) formularioRegister!: Function|undefined @Inject({ default: undefined }) formularioDeregister!: Function|undefined @Inject({ default: () => (): Record => ({}) }) getFormValues!: Function @@ -56,68 +49,45 @@ export default class FormularioInput extends Vue { @Model('input', { default: '' }) formularioValue: any - @Prop({ - type: [String, Number, Boolean], - default: false, - }) id!: string|number|boolean - - @Prop({ default: 'text' }) type!: string + @Prop({ default: null }) id!: string|number|null @Prop({ required: true }) name!: string @Prop({ default: false }) value!: any - - @Prop({ - default: '', - }) validation!: string|any[] - - @Prop({ - type: Object, - default: () => ({}), - }) validationRules!: Record - - @Prop({ - type: Object, - default: () => ({}), - }) validationMessages!: Record - + @Prop({ default: '' }) validation!: string|any[] + @Prop({ default: () => ({}) }) validationRules!: Record + @Prop({ default: () => ({}) }) validationMessages!: Record @Prop({ default: () => [] }) errors!: string[] @Prop({ - type: String, default: ERROR_BEHAVIOR.BLUR, - validator: value => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(value) + validator: behavior => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(behavior) }) errorBehavior!: string @Prop({ default: false }) showErrors!: boolean @Prop({ default: false }) disableErrors!: boolean @Prop({ default: true }) preventWindowDrops!: boolean - @Prop({ default: 'preview' }) imageBehavior!: string @Prop({ default: false }) uploader!: Function|Record|boolean @Prop({ default: false }) uploadUrl!: string|boolean @Prop({ default: 'live' }) uploadBehavior!: string defaultId: string = this.$formulario.nextId(this) - localErrors: string[] = [] proxy: Record = this.getInitialValue() - behavioralErrorVisibility: boolean = this.errorBehavior === 'live' - formShouldShowErrors = false + localErrors: string[] = [] validationErrors: ValidationError[] = [] pendingValidation: Promise = Promise.resolve() get context (): Record { - return this.defineModel({ + return Object.defineProperty({ id: this.id || this.defaultId, name: this.nameOrFallback, blurHandler: this.blurHandler.bind(this), errors: this.explicitErrors, allErrors: this.allErrors, - formShouldShowErrors: this.formShouldShowErrors, - imageBehavior: this.imageBehavior, performValidation: this.performValidation.bind(this), - showValidationErrors: this.showValidationErrors, - uploader: this.uploader || this.$formulario.getUploader(), validationErrors: this.validationErrors, value: this.value, - visibleValidationErrors: this.visibleValidationErrors, + }, 'model', { + get: this.modelGetter.bind(this), + set: this.modelSetter.bind(this), }) } @@ -151,13 +121,6 @@ export default class FormularioInput extends Vue { return this.allErrors.length > 0 } - /** - * Returns if form has actively visible errors (of any kind) - */ - get hasVisibleErrors (): boolean { - return (this.validationErrors && this.showValidationErrors) || this.explicitErrors.length > 0 - } - /** * The merged errors computed property. * Each error is an object with fields message (translated message), rule (rule name) and context @@ -169,13 +132,6 @@ export default class FormularioInput extends Vue { ] } - /** - * All of the currently visible validation errors (does not include error handling) - */ - get visibleValidationErrors (): ValidationError[] { - return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : [] - } - /** * These are errors we that have been explicitly passed to us. */ @@ -190,13 +146,6 @@ export default class FormularioInput extends Vue { return has(this.$options.propsData || {}, 'formularioValue') } - /** - * Determines if the field should show it's error (if it has one) - */ - get showValidationErrors (): boolean { - return this.showErrors || this.formShouldShowErrors || this.behavioralErrorVisibility - } - @Watch('proxy') onProxyChanged (newValue: Record, oldValue: Record): void { if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) { @@ -216,11 +165,6 @@ export default class FormularioInput extends Vue { } } - @Watch('showValidationErrors', { immediate: true }) - onShowValidationErrorsChanged (val: boolean): void { - this.$emit('error-visibility', val) - } - created (): void { this.applyInitialValue() if (this.formularioRegister && typeof this.formularioRegister === 'function') { @@ -244,16 +188,6 @@ export default class FormularioInput extends Vue { } } - /** - * Defines the model used throughout the existing context. - */ - defineModel (context: Record): Record { - return Object.defineProperty(context, 'model', { - get: this.modelGetter.bind(this), - set: this.modelSetter.bind(this), - }) - } - /** * Get the value from a model. */ @@ -283,8 +217,8 @@ export default class FormularioInput extends Vue { */ blurHandler (): void { this.$emit('blur') - if (this.errorBehavior === 'blur') { - this.behavioralErrorVisibility = true + if (this.errorBehavior === ERROR_BEHAVIOR.BLUR) { + this.performValidation() } } @@ -352,10 +286,13 @@ export default class FormularioInput extends Vue { const validationChanged = !shallowEqualObjects(violations, this.validationErrors) this.validationErrors = violations if (validationChanged) { - const errorBag = this.getErrorObject() + const errorBag = { + name: this.context.name, + errors: this.validationErrors, + } this.$emit('validation', errorBag) - if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') { - this.formularioFieldValidation(errorBag) + if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') { + this.onFormularioFieldValidation(errorBag) } } } @@ -398,16 +335,13 @@ export default class FormularioInput extends Vue { }) } - getErrorObject (): ValidationErrorBag { - return { - name: this.context.nameOrFallback || this.context.name, - errors: this.validationErrors.filter(s => typeof s === 'object'), - hasErrors: !!this.validationErrors.length - } - } - setErrors (errors: string[]): void { this.localErrors = arrayify(errors) } + + resetValidation (): void { + this.localErrors = [] + this.validationErrors = [] + } } diff --git a/src/axios.types.ts b/src/axios.types.ts deleted file mode 100644 index fd9eade..0000000 --- a/src/axios.types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface AxiosResponse { - data: any -} - -export interface AxiosError {} diff --git a/src/libs/registry.ts b/src/form/registry.ts similarity index 98% rename from src/libs/registry.ts rename to src/form/registry.ts index 9b2ade9..c79e7dd 100644 --- a/src/libs/registry.ts +++ b/src/form/registry.ts @@ -1,4 +1,4 @@ -import { shallowEqualObjects, has, getNested } from './utils' +import { shallowEqualObjects, has, getNested } from '@/libs/utils' import FormularioForm from '@/FormularioForm.vue' import FormularioInput from '@/FormularioInput.vue' diff --git a/src/libs/faux-uploader.ts b/src/libs/faux-uploader.ts deleted file mode 100644 index afb1a2f..0000000 --- a/src/libs/faux-uploader.ts +++ /dev/null @@ -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 { - 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() - }) -} diff --git a/src/libs/mimes.ts b/src/libs/mimes.ts deleted file mode 100644 index 7d8537b..0000000 --- a/src/libs/mimes.ts +++ /dev/null @@ -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' -} diff --git a/src/libs/utils.ts b/src/libs/utils.ts index a95f2ef..4f706d1 100644 --- a/src/libs/utils.ts +++ b/src/libs/utils.ts @@ -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, callback: Function): Record { - const obj: Record = {} - 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, objB: Record): boolean { if (objA === objB) { return true @@ -148,57 +131,6 @@ export function parseRules (validation: any[]|string, rules: any): any[] { }).filter(f => !!f) } -/** - * Given an array of rules, group them by bail signals. For example for this: - * bail|required|min:10|max:20 - * we would expect: - * [[required], [min], [max]] - * because any sub-array failure would cause a shutdown. While - * ^required|min:10|max:10 - * would return: - * [[required], [min, max]] - * and no bailing would produce: - * [[required, min, max]] - */ -export function groupBails (rules: any[]): any[] { - const groups = [] - const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail') - if (bailIndex >= 0) { - // Get all the rules until the first bail rule (dont include the bail) - const preBail = rules.splice(0, bailIndex + 1).slice(0, -1) - // Rules before the `bail` rule are non-bailing - preBail.length && groups.push(preBail) - // All remaining rules are bailing rule groups - rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true }))) - } else { - groups.push(rules) - } - - return groups.reduce((groups, group) => { - // @ts-ignore - const splitByMod = (group, bailGroup = false) => { - if (group.length < 2) { - return Object.defineProperty([group], 'bail', { value: bailGroup }) - } - const splits = [] - // @ts-ignore - const modIndex = group.findIndex(([,,, modifier]) => modifier === '^') - if (modIndex >= 0) { - const preMod = group.splice(0, modIndex) - // rules before the modifier are non-bailing rules. - preMod.length && splits.push(...splitByMod(preMod, bailGroup)) - splits.push(Object.defineProperty([group.shift()], 'bail', { value: true })) - // rules after the modifier are non-bailing rules. - group.length && splits.push(...splitByMod(group, bailGroup)) - } else { - splits.push(group) - } - return splits - } - return groups.concat(splitByMod(group)) - }, []) -} - /** * Escape a string for use in regular expressions. */ @@ -255,7 +187,7 @@ export function cloneDeep (value: any): any { for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { - if (isScalar(value[key]) || value[key] instanceof FileUpload) { + if (isScalar(value[key])) { copy[key] = value[key] } else { copy[key] = cloneDeep(value[key]) diff --git a/src/validation/rules.ts b/src/validation/rules.ts index 814515f..0ba29bf 100644 --- a/src/validation/rules.ts +++ b/src/validation/rules.ts @@ -1,12 +1,7 @@ -// @ts-ignore import isUrl from 'is-url' -import FileUpload from '../FileUpload' import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils' import { ValidatableData } from '@/validation/types' -/** - * Library of rules - */ export default { /** * Rule: the value must be "yes", "on", "1", or true @@ -155,21 +150,6 @@ export default { })) }, - /** - * Check the file type is correct. - */ - mime ({ value }: { value: any }, ...types: string[]): Promise { - if (value instanceof FileUpload) { - const files = value.getFiles() - const isMimeCorrect = (file: File): boolean => types.includes(file.type) - const allValid: boolean = files.reduce((valid: boolean, { file }) => valid && isMimeCorrect(file), true) - - return Promise.resolve(allValid) - } - - return Promise.resolve(true) - }, - /** * Check the minimum value of a particular. */ @@ -239,9 +219,6 @@ export default { if (Array.isArray(value)) { return !!value.length } - if (value instanceof FileUpload) { - return value.getFiles().length > 0 - } if (typeof value === 'string') { return !!value } diff --git a/src/validation/types.ts b/src/validation/types.ts index 3cecae8..65108ca 100644 --- a/src/validation/types.ts +++ b/src/validation/types.ts @@ -31,5 +31,4 @@ export interface ValidationError { export interface ValidationErrorBag { name: string; errors: ValidationError[]; - hasErrors: boolean; } diff --git a/test/unit/Formulario.test.js b/test/unit/Formulario.test.js index 8fd3204..813de14 100644 --- a/test/unit/Formulario.test.js +++ b/test/unit/Formulario.test.js @@ -2,11 +2,6 @@ import Formulario from '@/index.ts' describe('Formulario', () => { it('Installs on vue instance', () => { - const components = [ - 'FormularioForm', - 'FormularioInput', - 'FormularioGrouping', - ] const registry = [] function Vue () {} Vue.component = function (name, instance) { @@ -14,6 +9,10 @@ describe('Formulario', () => { } Formulario.install(Vue) expect(Vue.prototype.$formulario).toBe(Formulario) - expect(registry).toEqual(components) + expect(registry).toEqual([ + 'FormularioForm', + 'FormularioGrouping', + 'FormularioInput', + ]) }) }) diff --git a/test/unit/FormularioForm.test.js b/test/unit/FormularioForm.test.js index 9d3e287..015239b 100644 --- a/test/unit/FormularioForm.test.js +++ b/test/unit/FormularioForm.test.js @@ -214,20 +214,7 @@ describe('FormularioForm', () => { expect(emitted['submit']).toBeTruthy() expect(emitted['submit'].length).toBe(1) - expect(emitted['submit'][0]).toStrictEqual([{ fieldName: 'Justin' }]) - }) - - it('accepts a values prop and uses it to set the initial values', async () => { - const wrapper = mount(FormularioForm, { - propsData: { values: { name: 'Dave Barnett', candy: true } }, - slots: { default: ` - - - - ` } - }) - await flushPromises() - expect(wrapper.find('input[type="text"]').element['value']).toBe('Dave Barnett') + expect(emitted['submit'][0]).toEqual([{ fieldName: 'Justin' }]) }) it('Receives a form-errors prop and displays it', async () => { diff --git a/test/unit/FormularioGrouping.test.js b/test/unit/FormularioGrouping.test.js index 7806f19..40448c7 100644 --- a/test/unit/FormularioGrouping.test.js +++ b/test/unit/FormularioGrouping.test.js @@ -10,7 +10,6 @@ Vue.use(Formulario) describe('FormularioGrouping', () => { it('Grouped fields to be set', async () => { const wrapper = mount(FormularioForm, { - propsData: { name: 'form' }, slots: { default: ` @@ -33,17 +32,16 @@ describe('FormularioGrouping', () => { expect(emitted['submit']).toBeTruthy() expect(emitted['submit'].length).toBe(1) - expect(emitted['submit'][0]).toStrictEqual([{ group: { text: 'test' } }]) + expect(emitted['submit'][0]).toEqual([{ group: { text: 'test' } }]) }) it('Grouped fields to be got', async () => { const wrapper = mount(FormularioForm, { propsData: { - name: 'form', formularioValue: { group: { text: 'Group text' }, text: 'Text', - } + }, }, slots: { default: ` @@ -79,23 +77,20 @@ describe('FormularioGrouping', () => { }) it('Errors are set for grouped fields', async () => { - const wrapper = mount({ - data: () => ({ values: {} }), - template: ` - + const wrapper = mount(FormularioForm, { + propsData: { + formularioValue: {}, + errors: { 'group.text': 'Test error' }, + }, + slots: { + default: ` - - - {{ error }} - + + {{ error }} - - ` + `, + }, }) expect(wrapper.findAll('span').length).toBe(1) }) diff --git a/test/unit/FormularioInput.test.js b/test/unit/FormularioInput.test.js index 481b628..8964f8b 100644 --- a/test/unit/FormularioInput.test.js +++ b/test/unit/FormularioInput.test.js @@ -182,74 +182,20 @@ describe('FormularioInput', () => { const errorObject = wrapper.emitted('validation')[0][0] expect(errorObject).toEqual({ name: 'testinput', - errors: [ - { - message: expect.any(String), - rule: expect.stringContaining('required'), - context: expect.any(Object) - } - ], - hasErrors: true + errors: [{ + rule: expect.stringContaining('required'), + context: expect.any(Object), + message: expect.any(String), + }], }) }) - it('emits a error-visibility event on blur', async () => { - const wrapper = mount(FormularioInput, { - propsData: { - validation: 'required', - errorBehavior: 'blur', - value: '', - name: 'testinput', - }, - scopedSlots: { - default: `` - } - }) - 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: `` - } - }) - await flushPromises() - expect(wrapper.emitted('error-visibility').length).toBe(1) - wrapper.find('input[type="text"]').setValue('bar') - await flushPromises() - expect(wrapper.emitted('error-visibility').length).toBe(1) - }) - - it('can bail on validation when encountering the bail rule', async () => { + it('Can bail on validation when encountering the bail rule', async () => { const wrapper = mount(FormularioInput, { propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1); + expect(wrapper.vm.context.validationErrors.length).toBe(1); }) it('can show multiple validation errors if they occur before the bail rule', async () => { @@ -257,7 +203,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2); + expect(wrapper.vm.context.validationErrors.length).toBe(2); }) it('can avoid bail behavior by using modifier', async () => { @@ -265,7 +211,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' } }) await flushPromises(); - expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2); + expect(wrapper.vm.context.validationErrors.length).toBe(2); }) it('prevents later error messages when modified rule fails', async () => { @@ -273,7 +219,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1); + expect(wrapper.vm.context.validationErrors.length).toBe(1); }) it('can bail in the middle of the rule set with a modifier', async () => { @@ -281,7 +227,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2); + expect(wrapper.vm.context.validationErrors.length).toBe(2); }) it('does not show errors on blur when set error-behavior is submit', async () => { diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index 63506eb..d663f2a 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -1,6 +1,5 @@ -import { parseRules, parseLocale, regexForFormat, cloneDeep, isScalar, snakeToCamel, groupBails } from '@/libs/utils' +import { cloneDeep, isScalar, parseRules, regexForFormat, snakeToCamel } from '@/libs/utils' import rules from '@/validation/rules.ts' -import FileUpload from '@/FileUpload' describe('parseRules', () => { it('parses single string rules, returning empty arguments array', () => { @@ -113,8 +112,6 @@ describe('isScalar', () => { it('passes on undefined', () => expect(isScalar(undefined)).toBe(true)) it('fails on pojo', () => expect(isScalar({})).toBe(false)) - - it('fails on custom type', () => expect(isScalar(FileUpload)).toBe(false)) }) describe('cloneDeep', () => { @@ -175,83 +172,3 @@ describe('snakeToCamel', () => { expect(snakeToCamel(fn)).toBe(fn) }) }) - - -describe('parseLocale', () => { - it('properly orders the options', () => { - expect(parseLocale('en-US-VA')).toEqual(['en-US-VA', 'en-US', 'en']) - }) - - it('properly parses a single option', () => { - expect(parseLocale('en')).toEqual(['en']) - }) -}) - -describe('groupBails', () => { - it('wraps non bailed rules in an array', () => { - const bailGroups = groupBails([[,,'required'], [,,'min']]) - expect(bailGroups).toEqual( - [ [[,,'required'], [,,'min']] ] // dont bail on either of these - ) - expect(bailGroups.map(group => !!group.bail)).toEqual([false]) - }) - - it('splits bailed rules into two arrays array', () => { - const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'bail'], [,, 'matches'], [,,'min']]) - expect(bailGroups).toEqual([ - [ [,,'required'], [,,'max'] ], // dont bail on these - [ [,, 'matches'] ], // bail on this one - [ [,,'min'] ] // bail on this one - ]) - expect(bailGroups.map(group => !!group.bail)).toEqual([false, true, true]) - }) - - it('splits entire rule set when bail is at the beginning', () => { - const bailGroups = groupBails([[,, 'bail'], [,,'required'], [,,'max'], [,, 'matches'], [,,'min']]) - expect(bailGroups).toEqual([ - [ [,, 'required'] ], // bail on this one - [ [,, 'max'] ], // bail on this one - [ [,, 'matches'] ], // bail on this one - [ [,, 'min'] ] // bail on this one - ]) - expect(bailGroups.map(group => !!group.bail)).toEqual([true, true, true, true]) - }) - - it('splits no rules when bail is at the end', () => { - const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'matches'], [,,'min'], [,, 'bail']]) - expect(bailGroups).toEqual([ - [ [,, 'required'], [,, 'max'], [,, 'matches'], [,, 'min'] ] // dont bail on these - ]) - expect(bailGroups.map(group => !!group.bail)).toEqual([false]) - }) - - it('splits individual modified names into two groups when at the begining', () => { - const bailGroups = groupBails([[,,'required', '^'], [,,'max'], [,, 'matches'], [,,'min'] ]) - expect(bailGroups).toEqual([ - [ [,, 'required', '^'] ], // bail on this one - [ [,, 'max'], [,, 'matches'], [,, 'min'] ] // dont bail on these - ]) - expect(bailGroups.map(group => !!group.bail)).toEqual([true, false]) - }) - - it('splits individual modified names into three groups when in the middle', () => { - const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'matches', '^'], [,,'min'] ]) - expect(bailGroups).toEqual([ - [ [,, 'required'], [,, 'max'] ], // dont bail on these - [ [,, 'matches', '^'] ], // bail on this one - [ [,, 'min'] ] // dont bail on this - ]) - expect(bailGroups.map(group => !!group.bail)).toEqual([false, true, false]) - }) - - it('splits individual modified names into four groups when used twice', () => { - const bailGroups = groupBails([[,,'required', '^'], [,,'max'], [,, 'matches', '^'], [,,'min'] ]) - expect(bailGroups).toEqual([ - [ [,, 'required', '^'] ], // bail on this - [ [,, 'max'] ], // dont bail on this - [ [,, 'matches', '^'] ], // bail on this - [ [,, 'min'] ] // dont bail on this - ]) - expect(bailGroups.map(group => !!group.bail)).toEqual([true, false, true, false]) - }) -}) diff --git a/test/unit/rules.test.js b/test/unit/validation/rules.test.js similarity index 95% rename from test/unit/rules.test.js rename to test/unit/validation/rules.test.js index b91ba00..c2414a2 100644 --- a/test/unit/rules.test.js +++ b/test/unit/validation/rules.test.js @@ -1,5 +1,4 @@ import rules from '@/validation/rules.ts' -import FileUpload from '../../src/FileUpload' /** @@ -320,32 +319,6 @@ describe('matches', () => { }) }) -/** - * Mime types. - */ -describe('mime', () => { - it('passes basic image/jpeg stack', async () => { - const fileUpload = new FileUpload({ - files: [ { type: 'image/jpeg' } ] - }) - expect(await rules.mime({ value: fileUpload }, 'image/png', 'image/jpeg')).toBe(true) - }) - - it('passes when match is at begining of stack', async () => { - const fileUpload = new FileUpload({ - files: [ { type: 'document/pdf' } ] - }) - expect(await rules.mime({ value: fileUpload }, 'document/pdf')).toBe(true) - }) - - it('fails when not in stack', async () => { - const fileUpload = new FileUpload({ - files: [ { type: 'application/json' } ] - }) - expect(await rules.mime({ value: fileUpload }, 'image/png', 'image/jpeg')).toBe(false) - }) -}) - /** * Minimum. */ @@ -459,10 +432,6 @@ describe('required', () => { it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true)) it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true)) - - it('passes with FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [{ name: 'j.png' }] }) })).toBe(true)) - - it('fails with empty FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [] }) })).toBe(false)) }) /** diff --git a/test/unit/validation/validator.test.js b/test/unit/validation/validator.test.js new file mode 100644 index 0000000..e80cc7e --- /dev/null +++ b/test/unit/validation/validator.test.js @@ -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, + }]) + }) +})