diff --git a/.eslintrc.js b/.eslintrc.js index cf23a56..ed331bd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { root: true, parserOptions: { - parser: 'babel-eslint', + parser: '@typescript-eslint/parser', sourceType: 'module', }, @@ -10,6 +10,7 @@ module.exports = { extends: [ 'standard', '@vue/standard', + '@vue/typescript', 'plugin:vue/recommended', ], @@ -21,12 +22,14 @@ module.exports = { // allow paren-less arrow functions 'arrow-parens': 0, 'comma-dangle': ['error', 'only-multiline'], - 'indent': ['error', 4], + 'indent': ['error', 4, { SwitchCase: 1 }], 'max-depth': ['error', 3], 'max-lines-per-function': ['error', 40], 'no-console': ['warn', {allow: ['warn', 'error']}], // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error'], 'vue/html-closing-bracket-spacing': ['error', { startTag: 'never', endTag: 'never', diff --git a/package.json b/package.json index 81e28df..9c22c9f 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,31 @@ { "name": "@retailcrm/vue-formulario", "version": "0.1.0", + "author": "RetailDriverLLC ", + "scripts": { + "build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulario.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulario.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulario.min.js | wc -c)b gzip\"", + "test": "NODE_ENV=test jest --config test/jest.conf.js", + "build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js", + "build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js", + "build:size": "gzip -c dist/formulario.esm.js | wc -c", + "build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js", + "dev": "vue-cli-service serve --port=7872 examples/main.js", + "test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage", + "test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch" + }, "main": "dist/formulario.umd.js", "module": "dist/formulario.esm.js", - "unpkg": "dist/formulario.min.js", - "publishConfig": { - "access": "public" - }, "browser": { "./sfc": "src/Formulario.js" }, - "scripts": { - "build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulario.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulario.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulario.min.js | wc -c)b gzip\"", - "build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js", - "build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js", - "build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js", - "test": "NODE_ENV=test jest --config test/jest.conf.js", - "test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch", - "test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage", - "build:size": "gzip -c dist/formulario.esm.js | wc -c", - "dev": "vue-cli-service serve --port=7872 examples/main.js" - }, - "repository": { - "type": "git", - "url": "git+ssh://git@github.com/retailcrm/vue-formulario.git" - }, - "keywords": [ - "vue", - "form", - "forms", - "validation", - "validate" - ], - "author": "RetailDriverLLC ", - "contributors": [ - "Justin Schroeder " - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/retailcrm/vue-formulario/issues" + "unpkg": "dist/formulario.min.js", + "dependencies": { + "is-plain-object": "^3.0.0", + "is-url": "^1.2.4", + "nanoid": "^2.1.11", + "vue-class-component": "^7.2.3", + "vue-i18n": "^8.17.7", + "vue-property-decorator": "^8.4.2" }, "devDependencies": { "@babel/core": "^7.9.6", @@ -47,11 +34,18 @@ "@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-node-resolve": "^7.1.3", + "@types/is-url": "^1.2.28", + "@types/jest": "^26.0.14", + "@types/nanoid": "^2.1.0", + "@typescript-eslint/eslint-plugin": "^2.26.0", + "@typescript-eslint/parser": "^2.26.0", "@vue/cli-plugin-babel": "^4.3.1", "@vue/cli-plugin-eslint": "^4.3.1", + "@vue/cli-plugin-typescript": "^4.5.7", "@vue/cli-service": "^4.5.4", "@vue/component-compiler-utils": "^3.1.2", "@vue/eslint-config-standard": "^5.1.2", + "@vue/eslint-config-typescript": "^5.0.2", "@vue/test-utils": "^1.0.2", "autoprefixer": "^9.7.6", "babel-core": "^7.0.0-bridge.0", @@ -65,7 +59,7 @@ "eslint-plugin-standard": "^4.0.0", "eslint-plugin-vue": "^5.2.3", "flush-promises": "^1.0.2", - "jest": "^25.5.4", + "jest": "^26.5.2", "jest-vue-preprocessor": "^1.7.1", "rollup": "^1.32.1", "rollup-plugin-auto-external": "^2.0.0", @@ -73,7 +67,8 @@ "rollup-plugin-multi-input": "^1.1.1", "rollup-plugin-terser": "^5.3.0", "rollup-plugin-vue": "^5.1.7", - "typescript": "^3.9.2", + "ts-jest": "^26.4.1", + "typescript": "~3.9.3", "vue": "^2.6.11", "vue-jest": "^3.0.5", "vue-runtime-helpers": "^1.1.2", @@ -81,10 +76,25 @@ "vue-template-es2015-compiler": "^1.9.1", "watch": "^1.0.2" }, - "dependencies": { - "is-plain-object": "^3.0.0", - "is-url": "^1.2.4", - "nanoid": "^2.1.11", - "vue-i18n": "^8.17.7" + "bugs": { + "url": "https://github.com/retailcrm/vue-formulario/issues" + }, + "contributors": [ + "Justin Schroeder " + ], + "keywords": [ + "vue", + "form", + "forms", + "validation", + "validate" + ], + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/retailcrm/vue-formulario.git" } } diff --git a/src/FileUpload.js b/src/FileUpload.ts similarity index 78% rename from src/FileUpload.js rename to src/FileUpload.ts index cf2226a..6210268 100644 --- a/src/FileUpload.js +++ b/src/FileUpload.ts @@ -1,4 +1,27 @@ import nanoid from 'nanoid/non-secure' +import { AxiosResponse, AxiosError } from '@/axios.types' +import { ObjectType } from '@/common.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 /** @@ -6,13 +29,14 @@ import nanoid from 'nanoid/non-secure' * the upload flow. */ class FileUpload { - /** - * Create a file upload object. - * @param {FileList} input - * @param {object} context - * @param {object} options - */ - constructor (input, context, options = {}) { + public input: DataTransfer + public fileList: FileList + public files: FileItem[] + public options: ObjectType + public context: ObjectType + public results: any[] | boolean + + constructor (input: DataTransfer, context: ObjectType, options: ObjectType = {}) { this.input = input this.fileList = input.files this.files = [] @@ -30,7 +54,7 @@ class FileUpload { * 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) { + rehydrateFileList (items: any[]) { const fauxFileList = items.reduce((fileList, item) => { const key = this.options ? this.options.fileUrlKey : 'url' const url = item[key] @@ -51,9 +75,9 @@ class FileUpload { * Produce an array of files and alert the callback. * @param {FileList} fileList */ - addFileList (fileList) { + addFileList (fileList: FileList) { for (let i = 0; i < fileList.length; i++) { - const file = fileList[i] + const file: File = fileList[i] const uuid = nanoid() this.files.push({ progress: false, @@ -65,6 +89,7 @@ class FileUpload { uuid, path: false, removeFile: () => this.removeFile(uuid), + // @ts-ignore previewData: file.previewData || false }) } @@ -94,24 +119,24 @@ class FileUpload { /** * Get a new uploader function. */ - getUploader (...args) { + getUploader (...args: [File, ProgressSetter, ErrorHandler, ObjectType]) { if (this.uploaderIsAxios()) { - const formData = new FormData() - formData.append(this.context.name || 'file', args[0]) + 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, formData, { + return this.context.uploader.post(this.context.uploadUrl, data, { headers: { - 'Content-Type': 'multipart/form-data' + 'Content-Type': 'multipart/form-data', }, - onUploadProgress: progressEvent => { + onUploadProgress: (event: ProgressEvent) => { // args[1] here is the upload progress handler function - args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total)) + args[1](Math.round((event.loaded * 100) / event.total)) } }) - .then(res => res.data) - .catch(err => args[2](err)) + .then((response: AxiosResponse) => response.data) + .catch(args[2]) } return this.context.uploader(...args) } @@ -130,7 +155,7 @@ class FileUpload { Promise.all(this.files.map(file => { return file.path ? Promise.resolve(file.path) : this.getUploader( file.file, - (progress) => { + progress => { file.progress = progress if (progress >= 100) { if (!file.complete) { @@ -140,7 +165,7 @@ class FileUpload { file.complete = true } }, - (error) => { + error => { file.progress = 0 file.error = error file.complete = true @@ -158,16 +183,15 @@ class FileUpload { /** * Remove a file from the uploader (and the file list) - * @param {string} uuid */ - removeFile (uuid) { + removeFile (uuid: string) { 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.file)) + this.files.map(({ file }) => transfer.items.add(file)) this.fileList = transfer.files - this.input.files = this.fileList + this.input = transfer } } @@ -178,6 +202,7 @@ class FileUpload { 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) } diff --git a/src/FormSubmission.js b/src/FormSubmission.ts similarity index 60% rename from src/FormSubmission.js rename to src/FormSubmission.ts index bbd1c58..0a0ddc7 100644 --- a/src/FormSubmission.js +++ b/src/FormSubmission.ts @@ -1,12 +1,15 @@ 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) { + constructor (form: FormularioForm) { this.form = form } @@ -16,7 +19,7 @@ export default class FormSubmission { * @return {Promise} resolves a boolean */ hasValidationErrors () { - return this.form.hasValidationErrors() + return (this.form as any).hasValidationErrors() } /** @@ -25,12 +28,17 @@ export default class FormSubmission { */ values () { return new Promise((resolve, reject) => { + const form = this.form as any const pending = [] - const values = cloneDeep(this.form.proxy) + const values = cloneDeep(form.proxy) + for (const key in values) { - if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) { + if ( + Object.prototype.hasOwnProperty.call(values, key) && + typeof form.proxy[key] === 'object' && + form.proxy[key] instanceof FileUpload) { pending.push( - this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data })) + form.proxy[key].upload().then((data: Object) => Object.assign(values, { [key]: data })) ) } } diff --git a/src/Formulario.js b/src/Formulario.ts similarity index 65% rename from src/Formulario.js rename to src/Formulario.ts index 1a9ff33..dc9061f 100644 --- a/src/Formulario.js +++ b/src/Formulario.ts @@ -1,21 +1,50 @@ +import { VueConstructor } from 'vue' + import library from './libs/library' import rules from './libs/rules' import mimes from './libs/mimes' import FileUpload from './FileUpload' import RuleValidationMessages from './RuleValidationMessages' -import { arrayify } from './libs/utils' +import { arrayify, has } from './libs/utils' import isPlainObject from 'is-plain-object' import fauxUploader from './libs/faux-uploader' -import FormularioForm from './FormularioForm.vue' -import FormularioInput from './FormularioInput.vue' +import FormularioForm from '@/FormularioForm.vue' +import FormularioInput from '@/FormularioInput.vue' import FormularioGrouping from './FormularioGrouping.vue' +import { ObjectType } from '@/common.types' +import { ValidationContext } from '@/validation.types' + +interface ErrorHandler { + (error: any, formName?: string): any +} + +interface FormularioOptions { + components?: { [name: string]: VueConstructor } + plugins?: any[] + library?: any + rules?: any + mimes?: any + locale?: any + uploader?: any + uploadUrl?: any + fileUrlKey?: any + errorHandler?: ErrorHandler + uploadJustCompleteDuration?: any + validationMessages?: any + idPrefix?: string +} // noinspection JSUnusedGlobalSymbols /** * The base formulario library. */ class Formulario { + public options: FormularioOptions + public defaults: FormularioOptions + public registry: Map + public idRegistry: { [name: string]: number } + /** * Instantiate our base options. */ @@ -25,7 +54,7 @@ class Formulario { components: { FormularioForm, FormularioInput, - FormularioGrouping + FormularioGrouping, }, library, rules, @@ -35,7 +64,7 @@ class Formulario { uploadUrl: false, fileUrlKey: 'url', uploadJustCompleteDuration: 1000, - errorHandler: (err) => err, + errorHandler: (error: any) => error, plugins: [RuleValidationMessages], validationMessages: {}, idPrefix: 'formulario-' @@ -47,10 +76,10 @@ class Formulario { /** * Install vue formulario, and register it’s components. */ - install (Vue, options) { + install (Vue: VueConstructor, options?: FormularioOptions) { Vue.prototype.$formulario = this this.options = this.defaults - let plugins = this.defaults.plugins + let plugins = this.defaults.plugins as any[] if (options && Array.isArray(options.plugins) && options.plugins.length) { plugins = plugins.concat(options.plugins) } @@ -69,22 +98,22 @@ class Formulario { * However, SSR and deterministic ids can be very challenging, so this * implementation is open to community review. */ - nextId (vm) { + nextId (vm: Vue) { + 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 (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) { this.idRegistry[pathPrefix] = 0 } - return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}` + return `${options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}` } /** * Given a set of options, apply them to the pre-existing options. - * @param {Object} extendWith */ - extend (extendWith) { + extend (extendWith: FormularioOptions) { if (typeof extendWith === 'object') { - this.options = this.merge(this.options, extendWith) + this.options = this.merge(this.options as FormularioOptions, extendWith) return this } throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`) @@ -98,11 +127,11 @@ class Formulario { * @param {Object} mergeWith * @param {boolean} concatArrays */ - merge (base, mergeWith, concatArrays = true) { - const merged = {} + merge (base: ObjectType, mergeWith: ObjectType, concatArrays: boolean = true) { + const merged: ObjectType = {} for (const key in base) { - if (Object.prototype.hasOwnProperty.call(mergeWith, key)) { + if (has(mergeWith, key)) { if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) { merged[key] = this.merge(base[key], mergeWith[key], concatArrays) } else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) { @@ -116,7 +145,7 @@ class Formulario { } for (const prop in mergeWith) { - if (!Object.prototype.hasOwnProperty.call(merged, prop)) { + if (!has(merged, prop)) { merged[prop] = mergeWith[prop] } } @@ -126,10 +155,9 @@ class Formulario { /** * Determine what "class" of input this element is given the "type". - * @param {string} type */ - classify (type) { - if (Object.prototype.hasOwnProperty.call(this.options.library, type)) { + classify (type: string) { + if (has(this.options.library, type)) { return this.options.library[type].classification } @@ -138,10 +166,9 @@ class Formulario { /** * Determine what type of component to render given the "type". - * @param {string} type */ - component (type) { - if (Object.prototype.hasOwnProperty.call(this.options.library, type)) { + component (type: string) { + if (has(this.options.library, type)) { return this.options.library[type].component } @@ -150,59 +177,60 @@ class Formulario { /** * Get validation rules by merging any passed in with global rules. - * @return {object} object of validation functions */ - rules (rules = {}) { + rules (rules: Object = {}) { return { ...this.options.rules, ...rules } } /** * Get the validation message for a particular error. */ - validationMessage (rule, validationContext, vm) { - if (Object.prototype.hasOwnProperty.call(this.options.validationMessages, rule)) { - return this.options.validationMessages[rule](vm, validationContext) + validationMessage (rule: string, context: ValidationContext, vm: Vue) { + if (has(this.options.validationMessages, rule)) { + return this.options.validationMessages[rule](vm, context) } else { - return this.options.validationMessages.default(vm, validationContext) + return this.options.validationMessages.default(vm, context) } } /** * Given an instance of a FormularioForm register it. - * @param {Vue} form */ - register (form) { + 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. - * @param {Vue} form */ - deregister (form) { + deregister (form: FormularioForm) { if ( form.$options.name === 'FormularioForm' && + // @ts-ignore form.name && - this.registry.has(form.name) + // @ts-ignore + this.registry.has(form.name as string) ) { - this.registry.delete(form.name) + // @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. - * - * @param {error} err - * @param {string} formName - * @param {boolean} skip */ - handle (err, formName, skip = false) { - const e = skip ? err : this.options.errorHandler(err, formName) + handle (error: any, formName: string, skip: boolean = false) { + // @ts-ignore + const e = skip ? error : this.options.errorHandler(error, formName) if (formName && this.registry.has(formName)) { - this.registry.get(formName).applyErrors({ + const form = this.registry.get(formName) as FormularioForm + // @ts-ignore + form.applyErrors({ formErrors: arrayify(e.formErrors), inputErrors: e.inputErrors || {} }) @@ -212,33 +240,32 @@ class Formulario { /** * Reset a form. - * @param {string} formName - * @param {object} initialValue */ - reset (formName, initialValue = {}) { + reset (formName: string, initialValue: Object = {}) { this.resetValidation(formName) this.setValues(formName, initialValue) } /** * Reset the form's validation messages. - * @param {string} formName */ - resetValidation (formName) { - const form = this.registry.get(formName) + 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. - * @param {string} formName - * @param {object} values */ - setValues (formName, values) { - if (values && !Array.isArray(values) && typeof values === 'object') { - const form = this.registry.get(formName) + setValues (formName: string, values?: ObjectType) { + if (values) { + const form = this.registry.get(formName) as FormularioForm + // @ts-ignore form.setValues({ ...values }) } } @@ -268,9 +295,11 @@ class Formulario { /** * Create a new instance of an upload. */ - createUpload (fileList, context) { - return new FileUpload(fileList, context, this.options) + createUpload (data: DataTransfer, context: ObjectType) { + return new FileUpload(data, context, this.options) } } +export { Formulario } + export default new Formulario() diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue index 737290d..7d68ae0 100644 --- a/src/FormularioForm.vue +++ b/src/FormularioForm.vue @@ -7,185 +7,271 @@ - diff --git a/src/FormularioGrouping.vue b/src/FormularioGrouping.vue index 32b2629..73221aa 100644 --- a/src/FormularioGrouping.vue +++ b/src/FormularioGrouping.vue @@ -7,42 +7,36 @@ - diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index f7e9efd..7806652 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -14,201 +14,301 @@ -