1
0
mirror of synced 2024-11-22 05:16:05 +03:00

refactor: Rewritten onto typescript@3.9

This commit is contained in:
Zaytsev Kirill 2020-10-10 22:45:28 +03:00
parent acb6357433
commit 63a7c88905
33 changed files with 2824 additions and 2022 deletions

View File

@ -2,7 +2,7 @@ module.exports = {
root: true, root: true,
parserOptions: { parserOptions: {
parser: 'babel-eslint', parser: '@typescript-eslint/parser',
sourceType: 'module', sourceType: 'module',
}, },
@ -10,6 +10,7 @@ module.exports = {
extends: [ extends: [
'standard', 'standard',
'@vue/standard', '@vue/standard',
'@vue/typescript',
'plugin:vue/recommended', 'plugin:vue/recommended',
], ],
@ -21,12 +22,14 @@ module.exports = {
// allow paren-less arrow functions // allow paren-less arrow functions
'arrow-parens': 0, 'arrow-parens': 0,
'comma-dangle': ['error', 'only-multiline'], 'comma-dangle': ['error', 'only-multiline'],
'indent': ['error', 4], 'indent': ['error', 4, { SwitchCase: 1 }],
'max-depth': ['error', 3], 'max-depth': ['error', 3],
'max-lines-per-function': ['error', 40], 'max-lines-per-function': ['error', 40],
'no-console': ['warn', {allow: ['warn', 'error']}], 'no-console': ['warn', {allow: ['warn', 'error']}],
// allow debugger during development // allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, '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', { 'vue/html-closing-bracket-spacing': ['error', {
startTag: 'never', startTag: 'never',
endTag: 'never', endTag: 'never',

View File

@ -1,44 +1,31 @@
{ {
"name": "@retailcrm/vue-formulario", "name": "@retailcrm/vue-formulario",
"version": "0.1.0", "version": "0.1.0",
"author": "RetailDriverLLC <integration@retailcrm.ru>",
"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", "main": "dist/formulario.umd.js",
"module": "dist/formulario.esm.js", "module": "dist/formulario.esm.js",
"unpkg": "dist/formulario.min.js",
"publishConfig": {
"access": "public"
},
"browser": { "browser": {
"./sfc": "src/Formulario.js" "./sfc": "src/Formulario.js"
}, },
"scripts": { "unpkg": "dist/formulario.min.js",
"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\"", "dependencies": {
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js", "is-plain-object": "^3.0.0",
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js", "is-url": "^1.2.4",
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js", "nanoid": "^2.1.11",
"test": "NODE_ENV=test jest --config test/jest.conf.js", "vue-class-component": "^7.2.3",
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch", "vue-i18n": "^8.17.7",
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage", "vue-property-decorator": "^8.4.2"
"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 <integration@retailcrm.ru>",
"contributors": [
"Justin Schroeder <justin@wearebraid.com>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/retailcrm/vue-formulario/issues"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.6", "@babel/core": "^7.9.6",
@ -47,11 +34,18 @@
"@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-node-resolve": "^7.1.3", "@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-babel": "^4.3.1",
"@vue/cli-plugin-eslint": "^4.3.1", "@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-plugin-typescript": "^4.5.7",
"@vue/cli-service": "^4.5.4", "@vue/cli-service": "^4.5.4",
"@vue/component-compiler-utils": "^3.1.2", "@vue/component-compiler-utils": "^3.1.2",
"@vue/eslint-config-standard": "^5.1.2", "@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^1.0.2", "@vue/test-utils": "^1.0.2",
"autoprefixer": "^9.7.6", "autoprefixer": "^9.7.6",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
@ -65,7 +59,7 @@
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.3", "eslint-plugin-vue": "^5.2.3",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"jest": "^25.5.4", "jest": "^26.5.2",
"jest-vue-preprocessor": "^1.7.1", "jest-vue-preprocessor": "^1.7.1",
"rollup": "^1.32.1", "rollup": "^1.32.1",
"rollup-plugin-auto-external": "^2.0.0", "rollup-plugin-auto-external": "^2.0.0",
@ -73,7 +67,8 @@
"rollup-plugin-multi-input": "^1.1.1", "rollup-plugin-multi-input": "^1.1.1",
"rollup-plugin-terser": "^5.3.0", "rollup-plugin-terser": "^5.3.0",
"rollup-plugin-vue": "^5.1.7", "rollup-plugin-vue": "^5.1.7",
"typescript": "^3.9.2", "ts-jest": "^26.4.1",
"typescript": "~3.9.3",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-jest": "^3.0.5", "vue-jest": "^3.0.5",
"vue-runtime-helpers": "^1.1.2", "vue-runtime-helpers": "^1.1.2",
@ -81,10 +76,25 @@
"vue-template-es2015-compiler": "^1.9.1", "vue-template-es2015-compiler": "^1.9.1",
"watch": "^1.0.2" "watch": "^1.0.2"
}, },
"dependencies": { "bugs": {
"is-plain-object": "^3.0.0", "url": "https://github.com/retailcrm/vue-formulario/issues"
"is-url": "^1.2.4", },
"nanoid": "^2.1.11", "contributors": [
"vue-i18n": "^8.17.7" "Justin Schroeder <justin@wearebraid.com>"
],
"keywords": [
"vue",
"form",
"forms",
"validation",
"validate"
],
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
} }
} }

View File

@ -1,4 +1,27 @@
import nanoid from 'nanoid/non-secure' 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 // noinspection JSUnusedGlobalSymbols
/** /**
@ -6,13 +29,14 @@ import nanoid from 'nanoid/non-secure'
* the upload flow. * the upload flow.
*/ */
class FileUpload { class FileUpload {
/** public input: DataTransfer
* Create a file upload object. public fileList: FileList
* @param {FileList} input public files: FileItem[]
* @param {object} context public options: ObjectType
* @param {object} options public context: ObjectType
*/ public results: any[] | boolean
constructor (input, context, options = {}) {
constructor (input: DataTransfer, context: ObjectType, options: ObjectType = {}) {
this.input = input this.input = input
this.fileList = input.files this.fileList = input.files
this.files = [] this.files = []
@ -30,7 +54,7 @@ class FileUpload {
* Given a pre-existing array of files, create a faux FileList. * Given a pre-existing array of files, create a faux FileList.
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }] * @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
*/ */
rehydrateFileList (items) { rehydrateFileList (items: any[]) {
const fauxFileList = items.reduce((fileList, item) => { const fauxFileList = items.reduce((fileList, item) => {
const key = this.options ? this.options.fileUrlKey : 'url' const key = this.options ? this.options.fileUrlKey : 'url'
const url = item[key] const url = item[key]
@ -51,9 +75,9 @@ class FileUpload {
* Produce an array of files and alert the callback. * Produce an array of files and alert the callback.
* @param {FileList} fileList * @param {FileList} fileList
*/ */
addFileList (fileList) { addFileList (fileList: FileList) {
for (let i = 0; i < fileList.length; i++) { for (let i = 0; i < fileList.length; i++) {
const file = fileList[i] const file: File = fileList[i]
const uuid = nanoid() const uuid = nanoid()
this.files.push({ this.files.push({
progress: false, progress: false,
@ -65,6 +89,7 @@ class FileUpload {
uuid, uuid,
path: false, path: false,
removeFile: () => this.removeFile(uuid), removeFile: () => this.removeFile(uuid),
// @ts-ignore
previewData: file.previewData || false previewData: file.previewData || false
}) })
} }
@ -94,24 +119,24 @@ class FileUpload {
/** /**
* Get a new uploader function. * Get a new uploader function.
*/ */
getUploader (...args) { getUploader (...args: [File, ProgressSetter, ErrorHandler, ObjectType]) {
if (this.uploaderIsAxios()) { if (this.uploaderIsAxios()) {
const formData = new FormData() const data = new FormData()
formData.append(this.context.name || 'file', args[0]) data.append(this.context.name || 'file', args[0])
if (this.context.uploadUrl === false) { if (this.context.uploadUrl === false) {
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props') 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: { 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] 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) .then((response: AxiosResponse) => response.data)
.catch(err => args[2](err)) .catch(args[2])
} }
return this.context.uploader(...args) return this.context.uploader(...args)
} }
@ -130,7 +155,7 @@ class FileUpload {
Promise.all(this.files.map(file => { Promise.all(this.files.map(file => {
return file.path ? Promise.resolve(file.path) : this.getUploader( return file.path ? Promise.resolve(file.path) : this.getUploader(
file.file, file.file,
(progress) => { progress => {
file.progress = progress file.progress = progress
if (progress >= 100) { if (progress >= 100) {
if (!file.complete) { if (!file.complete) {
@ -140,7 +165,7 @@ class FileUpload {
file.complete = true file.complete = true
} }
}, },
(error) => { error => {
file.progress = 0 file.progress = 0
file.error = error file.error = error
file.complete = true file.complete = true
@ -158,16 +183,15 @@ class FileUpload {
/** /**
* Remove a file from the uploader (and the file list) * 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.files = this.files.filter(file => file.uuid !== uuid)
this.context.performValidation() this.context.performValidation()
if (window && this.fileList instanceof FileList) { if (window && this.fileList instanceof FileList) {
const transfer = new DataTransfer() 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.fileList = transfer.files
this.input.files = this.fileList this.input = transfer
} }
} }
@ -178,6 +202,7 @@ class FileUpload {
this.files.map(file => { this.files.map(file => {
if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) { if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) {
const reader = new FileReader() const reader = new FileReader()
// @ts-ignore
reader.onload = e => Object.assign(file, { previewData: e.target.result }) reader.onload = e => Object.assign(file, { previewData: e.target.result })
reader.readAsDataURL(file.file) reader.readAsDataURL(file.file)
} }

View File

@ -1,12 +1,15 @@
import { cloneDeep } from './libs/utils' import { cloneDeep } from './libs/utils'
import FileUpload from './FileUpload' import FileUpload from './FileUpload'
import FormularioForm from '@/FormularioForm.vue'
export default class FormSubmission { export default class FormSubmission {
public form: FormularioForm
/** /**
* Initialize a formulario form. * Initialize a formulario form.
* @param {vm} form an instance of FormularioForm * @param {vm} form an instance of FormularioForm
*/ */
constructor (form) { constructor (form: FormularioForm) {
this.form = form this.form = form
} }
@ -16,7 +19,7 @@ export default class FormSubmission {
* @return {Promise} resolves a boolean * @return {Promise} resolves a boolean
*/ */
hasValidationErrors () { hasValidationErrors () {
return this.form.hasValidationErrors() return (this.form as any).hasValidationErrors()
} }
/** /**
@ -25,12 +28,17 @@ export default class FormSubmission {
*/ */
values () { values () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const form = this.form as any
const pending = [] const pending = []
const values = cloneDeep(this.form.proxy) const values = cloneDeep(form.proxy)
for (const key in values) { 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( 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 }))
) )
} }
} }

View File

@ -1,21 +1,50 @@
import { VueConstructor } from 'vue'
import library from './libs/library' import library from './libs/library'
import rules from './libs/rules' import rules from './libs/rules'
import mimes from './libs/mimes' import mimes from './libs/mimes'
import FileUpload from './FileUpload' import FileUpload from './FileUpload'
import RuleValidationMessages from './RuleValidationMessages' import RuleValidationMessages from './RuleValidationMessages'
import { arrayify } from './libs/utils' import { arrayify, has } from './libs/utils'
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import fauxUploader from './libs/faux-uploader' import fauxUploader from './libs/faux-uploader'
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 { 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 // noinspection JSUnusedGlobalSymbols
/** /**
* The base formulario library. * The base formulario library.
*/ */
class Formulario { class Formulario {
public options: FormularioOptions
public defaults: FormularioOptions
public registry: Map<string, FormularioForm>
public idRegistry: { [name: string]: number }
/** /**
* Instantiate our base options. * Instantiate our base options.
*/ */
@ -25,7 +54,7 @@ class Formulario {
components: { components: {
FormularioForm, FormularioForm,
FormularioInput, FormularioInput,
FormularioGrouping FormularioGrouping,
}, },
library, library,
rules, rules,
@ -35,7 +64,7 @@ class Formulario {
uploadUrl: false, uploadUrl: false,
fileUrlKey: 'url', fileUrlKey: 'url',
uploadJustCompleteDuration: 1000, uploadJustCompleteDuration: 1000,
errorHandler: (err) => err, errorHandler: (error: any) => error,
plugins: [RuleValidationMessages], plugins: [RuleValidationMessages],
validationMessages: {}, validationMessages: {},
idPrefix: 'formulario-' idPrefix: 'formulario-'
@ -47,10 +76,10 @@ class Formulario {
/** /**
* Install vue formulario, and register its components. * Install vue formulario, and register its components.
*/ */
install (Vue, options) { install (Vue: VueConstructor, options?: FormularioOptions) {
Vue.prototype.$formulario = this Vue.prototype.$formulario = this
this.options = this.defaults 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) { if (options && Array.isArray(options.plugins) && options.plugins.length) {
plugins = plugins.concat(options.plugins) plugins = plugins.concat(options.plugins)
} }
@ -69,22 +98,22 @@ 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) { nextId (vm: Vue) {
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 (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) { if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
this.idRegistry[pathPrefix] = 0 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. * Given a set of options, apply them to the pre-existing options.
* @param {Object} extendWith
*/ */
extend (extendWith) { extend (extendWith: FormularioOptions) {
if (typeof extendWith === 'object') { if (typeof extendWith === 'object') {
this.options = this.merge(this.options, extendWith) this.options = this.merge(this.options as FormularioOptions, 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})`)
@ -98,11 +127,11 @@ class Formulario {
* @param {Object} mergeWith * @param {Object} mergeWith
* @param {boolean} concatArrays * @param {boolean} concatArrays
*/ */
merge (base, mergeWith, concatArrays = true) { merge (base: ObjectType, mergeWith: ObjectType, concatArrays: boolean = true) {
const merged = {} const merged: ObjectType = {}
for (const key in base) { for (const key in base) {
if (Object.prototype.hasOwnProperty.call(mergeWith, key)) { if (has(mergeWith, key)) {
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) { if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
merged[key] = this.merge(base[key], mergeWith[key], concatArrays) merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) { } else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
@ -116,7 +145,7 @@ class Formulario {
} }
for (const prop in mergeWith) { for (const prop in mergeWith) {
if (!Object.prototype.hasOwnProperty.call(merged, prop)) { if (!has(merged, prop)) {
merged[prop] = mergeWith[prop] merged[prop] = mergeWith[prop]
} }
} }
@ -126,10 +155,9 @@ class Formulario {
/** /**
* Determine what "class" of input this element is given the "type". * Determine what "class" of input this element is given the "type".
* @param {string} type
*/ */
classify (type) { classify (type: string) {
if (Object.prototype.hasOwnProperty.call(this.options.library, type)) { if (has(this.options.library, type)) {
return this.options.library[type].classification return this.options.library[type].classification
} }
@ -138,10 +166,9 @@ class Formulario {
/** /**
* Determine what type of component to render given the "type". * Determine what type of component to render given the "type".
* @param {string} type
*/ */
component (type) { component (type: string) {
if (Object.prototype.hasOwnProperty.call(this.options.library, type)) { if (has(this.options.library, type)) {
return this.options.library[type].component return this.options.library[type].component
} }
@ -150,59 +177,60 @@ class Formulario {
/** /**
* Get validation rules by merging any passed in with global rules. * 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 } return { ...this.options.rules, ...rules }
} }
/** /**
* Get the validation message for a particular error. * Get the validation message for a particular error.
*/ */
validationMessage (rule, validationContext, vm) { validationMessage (rule: string, context: ValidationContext, vm: Vue) {
if (Object.prototype.hasOwnProperty.call(this.options.validationMessages, rule)) { if (has(this.options.validationMessages, rule)) {
return this.options.validationMessages[rule](vm, validationContext) return this.options.validationMessages[rule](vm, context)
} else { } else {
return this.options.validationMessages.default(vm, validationContext) return this.options.validationMessages.default(vm, context)
} }
} }
/** /**
* Given an instance of a FormularioForm register it. * 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) { if (form.$options.name === 'FormularioForm' && form.name) {
// @ts-ignore
this.registry.set(form.name, form) this.registry.set(form.name, form)
} }
} }
/** /**
* Given an instance of a form, remove it from the registry. * Given an instance of a form, remove it from the registry.
* @param {Vue} form
*/ */
deregister (form) { deregister (form: FormularioForm) {
if ( if (
form.$options.name === 'FormularioForm' && form.$options.name === 'FormularioForm' &&
// @ts-ignore
form.name && 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 * Given an array, this function will attempt to make sense of the given error
* and hydrate a form with the resulting errors. * and hydrate a form with the resulting errors.
*
* @param {error} err
* @param {string} formName
* @param {boolean} skip
*/ */
handle (err, formName, skip = false) { handle (error: any, formName: string, skip: boolean = false) {
const e = skip ? err : this.options.errorHandler(err, formName) // @ts-ignore
const e = skip ? error : this.options.errorHandler(error, formName)
if (formName && this.registry.has(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), formErrors: arrayify(e.formErrors),
inputErrors: e.inputErrors || {} inputErrors: e.inputErrors || {}
}) })
@ -212,33 +240,32 @@ class Formulario {
/** /**
* Reset a form. * Reset a form.
* @param {string} formName
* @param {object} initialValue
*/ */
reset (formName, initialValue = {}) { reset (formName: string, initialValue: Object = {}) {
this.resetValidation(formName) this.resetValidation(formName)
this.setValues(formName, initialValue) this.setValues(formName, initialValue)
} }
/** /**
* Reset the form's validation messages. * Reset the form's validation messages.
* @param {string} formName
*/ */
resetValidation (formName) { resetValidation (formName: string) {
const form = this.registry.get(formName) const form = this.registry.get(formName) as FormularioForm
// @ts-ignore
form.hideErrors(formName) form.hideErrors(formName)
// @ts-ignore
form.namedErrors = [] form.namedErrors = []
// @ts-ignore
form.namedFieldErrors = {} form.namedFieldErrors = {}
} }
/** /**
* Set the form values. * Set the form values.
* @param {string} formName
* @param {object} values
*/ */
setValues (formName, values) { setValues (formName: string, values?: ObjectType) {
if (values && !Array.isArray(values) && typeof values === 'object') { if (values) {
const form = this.registry.get(formName) const form = this.registry.get(formName) as FormularioForm
// @ts-ignore
form.setValues({ ...values }) form.setValues({ ...values })
} }
} }
@ -268,9 +295,11 @@ class Formulario {
/** /**
* Create a new instance of an upload. * Create a new instance of an upload.
*/ */
createUpload (fileList, context) { createUpload (data: DataTransfer, context: ObjectType) {
return new FileUpload(fileList, context, this.options) return new FileUpload(data, context, this.options)
} }
} }
export { Formulario }
export default new Formulario() export default new Formulario()

View File

@ -7,185 +7,271 @@
</form> </form>
</template> </template>
<script> <script lang="ts">
import { arrayify, has } from './libs/utils' import Vue from 'vue'
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry' import {
Component,
Model,
Prop,
Provide,
Watch,
} from 'vue-property-decorator'
import { arrayify, getNested, has, setNested, shallowEqualObjects } from './libs/utils'
import Registry from './libs/registry'
import FormSubmission from './FormSubmission' import FormSubmission from './FormSubmission'
export default { @Component()
name: 'FormularioForm', export default class FormularioForm extends Vue {
@Provide() formularioFieldValidation (errorObject) {
return this.$emit('validation', errorObject)
}
provide () { @Provide() formularioRegister = this.register
@Provide() formularioDeregister = this.deregister
@Provide() formularioSetter = this.setFieldValue
@Provide() getFormValues = () => this.proxy
@Provide() observeErrors = this.addErrorObserver
@Provide() path: string = ''
@Provide() removeErrorObserver (observer) {
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
}
@Prop({
type: [String, Boolean],
default: false
}) public readonly name!: string | boolean
@Model('input', {
type: Object,
default: () => ({})
}) readonly formularioValue!: Object
@Prop({
type: [Object, Boolean],
default: false
}) readonly values!: Object | Boolean
@Prop({
type: [Object, Boolean],
default: false
}) readonly errors!: Object | Boolean
@Prop({
type: Array,
default: () => ([])
}) readonly formErrors!: []
public proxy: Object = {}
registry: Registry = new Registry(this)
childrenShouldShowErrors: boolean = false
formShouldShowErrors: boolean = false
errorObservers: [] = []
namedErrors: [] = []
namedFieldErrors: Object = {}
get classes () {
return { return {
...useRegistryProviders(this), 'formulario-form': true,
observeErrors: this.addErrorObserver, [`formulario-form--${this.name}`]: !!this.name
removeErrorObserver: this.removeErrorObserver,
formularioFieldValidation: this.formularioFieldValidation,
path: ''
} }
}, }
model: { get mergedFormErrors () {
prop: 'formularioValue', return this.formErrors.concat(this.namedErrors)
event: 'input' }
},
props: { get mergedFieldErrors () {
name: { const errors = {}
type: [String, Boolean],
default: false
},
formularioValue: { if (this.errors) {
type: Object, for (const fieldName in this.errors) {
default: () => ({}) errors[fieldName] = arrayify(this.errors[fieldName])
},
values: {
type: [Object, Boolean],
default: false
},
errors: {
type: [Object, Boolean],
default: false
},
formErrors: {
type: Array,
default: () => ([])
}
},
data () {
return {
...useRegistry(this),
formShouldShowErrors: false,
errorObservers: [],
namedErrors: [],
namedFieldErrors: {}
}
},
computed: {
...useRegistryComputed(),
classes () {
return {
'formulario-form': true,
[`formulario-form--${this.name}`]: !!this.name
} }
},
mergedFormErrors () {
return this.formErrors.concat(this.namedErrors)
},
mergedFieldErrors () {
const errors = {}
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
},
hasFormErrorObservers () {
return !!this.errorObservers.filter(o => o.type === 'form').length
} }
},
watch: { for (const fieldName in this.namedFieldErrors) {
formularioValue: { errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
handler (values) {
if (this.isVmodeled && values && typeof values === 'object') {
this.setValues(values)
}
},
deep: true
},
mergedFormErrors (errors) {
this.errorObservers
.filter(o => o.type === 'form')
.forEach(o => o.callback(errors))
},
mergedFieldErrors: {
handler (errors) {
this.errorObservers
.filter(o => o.type === 'input')
.forEach(o => o.callback(errors[o.field] || []))
},
immediate: true
} }
},
return errors
}
get hasFormErrorObservers () {
return this.errorObservers.some(o => o.type === 'form')
}
get hasInitialValue () {
return (
(this.formularioValue && typeof this.formularioValue === 'object') ||
(this.values && typeof this.values === 'object') ||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
)
}
get isVmodeled () {
return !!(Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formularioValue') &&
this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
}
get initialValues () {
if (
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
return Object.assign({}, 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 {}
}
@Watch('formularioValue', { deep: true })
onFormularioValueChanged (values) {
if (this.isVmodeled && values && typeof values === 'object') {
this.setValues(values)
}
}
@Watch('mergedFormErrors')
onMergedFormErrorsChanged (errors) {
this.errorObservers
.filter(o => o.type === 'form')
.forEach(o => o.callback(errors))
}
@Watch('mergedFieldErrors', { immediate: true })
onMergedFieldErrorsChanged (errors) {
this.errorObservers
.filter(o => o.type === 'input')
.forEach(o => o.callback(errors[o.field] || []))
}
created () { created () {
this.$formulario.register(this) this.$formulario.register(this)
this.applyInitialValues() this.applyInitialValues()
}, }
destroyed () { destroyed () {
this.$formulario.deregister(this) this.$formulario.deregister(this)
}, }
methods: { // @TODO: Check FormularioForm, seems need an interface
...useRegistryMethods(), public register (field: string, component: FormularioForm) {
this.registry.register(field, component)
}
applyErrors ({ formErrors, inputErrors }) { public deregister (field: string) {
// given an object of errors, apply them to this form this.registry.remove(field)
this.namedErrors = formErrors }
this.namedFieldErrors = inputErrors
},
addErrorObserver (observer) { applyErrors ({ formErrors, inputErrors }) {
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) { // given an object of errors, apply them to this form
this.errorObservers.push(observer) this.namedErrors = formErrors
if (observer.type === 'form') { this.namedFieldErrors = inputErrors
observer.callback(this.mergedFormErrors) }
} else if (has(this.mergedFieldErrors, observer.field)) {
observer.callback(this.mergedFieldErrors[observer.field]) addErrorObserver (observer) {
} if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
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])
} }
},
removeErrorObserver (observer) {
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
},
registerErrorComponent (component) {
if (!this.errorComponents.includes(component)) {
this.errorComponents.push(component)
}
},
formSubmitted () {
// 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 => {
if (typeof data !== 'undefined') {
this.$emit('submit', data)
return data
}
return undefined
})
},
formularioFieldValidation (errorObject) {
this.$emit('validation', errorObject)
} }
} }
registerErrorComponent (component) {
if (!this.errorComponents.includes(component)) {
this.errorComponents.push(component)
}
}
formSubmitted () {
// 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 => {
if (typeof data !== 'undefined') {
this.$emit('submit', data)
return data
}
return undefined
})
}
applyInitialValues () {
if (this.hasInitialValue) {
this.proxy = this.initialValues
}
}
setFieldValue (field, value) {
if (value === undefined) {
const { [field]: value, ...proxy } = this.proxy
this.proxy = proxy
} else {
setNested(this.proxy, field, value)
}
this.$emit('input', Object.assign({}, this.proxy))
}
hasValidationErrors () {
return Promise.all(this.registry.reduce((resolvers, cmp, name) => {
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
return resolvers
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
}
showErrors () {
this.childrenShouldShowErrors = true
this.registry.map(input => {
input.formShouldShowErrors = true
})
}
hideErrors () {
this.childrenShouldShowErrors = false
this.registry.map(input => {
input.formShouldShowErrors = false
input.behavioralErrorVisibility = false
})
}
setValues (values) {
// Collect all keys, existing and incoming
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
keys.forEach(field => {
if (this.registry.has(field) &&
!shallowEqualObjects(getNested(values, field), getNested(this.proxy, field)) &&
!shallowEqualObjects(getNested(values, field), this.registry.get(field).proxy)
) {
this.setFieldValue(field, getNested(values, field))
this.registry.get(field).context.model = getNested(values, field)
}
})
this.applyInitialValues()
}
} }
</script> </script>

View File

@ -7,42 +7,36 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import Vue from 'vue'
name: 'FormularioGrouping', import { Component, Prop } from 'vue-property-decorator'
@Component({
provide () { provide () {
return { return {
path: this.groupPath path: this.groupPath,
} }
}, },
inject: ['path'], inject: ['path'],
})
export default class FormularioGrouping extends Vue {
@Prop({ required: true })
readonly name!: string
props: { @Prop({ default: false })
name: { readonly isArrayItem!: boolean
type: String,
required: true
},
isArrayItem: { get groupPath () {
type: Boolean, if (this.isArrayItem) {
default: false return `${this.path}[${this.name}]`
} }
},
computed: { if (this.path === '') {
groupPath () { return this.name
if (this.isArrayItem) {
return `${this.path}[${this.name}]`
}
if (this.path === '') {
return this.name
}
return `${this.path}.${this.name}`
} }
return `${this.path}.${this.name}`
} }
} }
</script> </script>

View File

@ -14,201 +14,301 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import context from './libs/context' import Vue from 'vue'
import {
Component,
Inject,
Model,
Prop,
Provide,
Watch,
} from 'vue-property-decorator'
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils' import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
import { ValidationError } from '@/validation.types'
export default { const ERROR_BEHAVIOR = {
name: 'FormularioInput', BLUR: 'blur',
LIVE: 'live',
SUBMIT: 'submit',
}
@Component({
inheritAttrs: false, inheritAttrs: false,
provide () {
return {
// Allows sub-components of this input to register arbitrary rules.
formularioRegisterRule: this.registerRule,
formularioRemoveRule: this.removeRule
}
},
inject: {
formularioSetter: { default: undefined },
formularioFieldValidation: { default: () => () => ({}) },
formularioRegister: { default: undefined },
formularioDeregister: { default: undefined },
getFormValues: { default: () => () => ({}) },
observeErrors: { default: undefined },
removeErrorObserver: { default: undefined },
path: { default: '' }
},
model: {
prop: 'formularioValue',
event: 'input'
},
props: { props: {
type: {
type: String,
default: 'text'
},
name: {
type: String,
required: true
},
/* eslint-disable */
formularioValue: {
default: ''
},
value: {
default: false
},
/* eslint-enable */
id: {
type: [String, Boolean, Number],
default: false
},
errors: {
type: [String, Array, Boolean],
default: false
},
validation: {
type: [String, Boolean, Array],
default: false
},
validationName: {
type: [String, Boolean],
default: false
},
errorBehavior: {
type: String,
default: 'blur',
validator (value) {
return ['blur', 'live', 'submit'].includes(value)
}
},
showErrors: {
type: Boolean,
default: false
},
imageBehavior: { imageBehavior: {
type: String, type: String,
default: 'preview' default: 'preview'
}, },
uploadUrl: {
type: [String, Boolean],
default: false
},
uploader: { uploader: {
type: [Function, Object, Boolean], type: [Function, Object, Boolean],
default: false default: false
}, },
uploadBehavior: {
type: String,
default: 'live'
},
preventWindowDrops: {
type: Boolean,
default: true
},
validationMessages: {
type: Object,
default: () => ({})
},
validationRules: {
type: Object,
default: () => ({})
},
disableErrors: {
type: Boolean,
default: false
}
}, },
})
export default class FormularioInput extends Vue {
@Inject({ default: undefined }) formularioSetter!: Function | undefined
@Inject({ default: () => () => ({}) }) formularioFieldValidation!: Function
@Inject({ default: undefined }) formularioRegister!: Function | undefined
@Inject({ default: undefined }) formularioDeregister!: Function | undefined
@Inject({ default: () => () => ({}) }) getFormValues!: Function
@Inject({ default: undefined }) observeErrors!: Function | undefined
@Inject({ default: undefined }) removeErrorObserver!: Function | undefined
@Inject({ default: '' }) path!: string
data () { @Provide() formularioRegisterRule = this.registerRule
return { @Provide() formularioRemoveRule = this.removeRule
defaultId: this.$formulario.nextId(this),
localAttributes: {}, @Model('input', {
localErrors: [], default: '',
proxy: this.getInitialValue(), }) formularioValue: any
behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false, @Prop({
validationErrors: [], type: [String, Number, Boolean],
pendingValidation: Promise.resolve(), default: false,
// These registries are used for injected messages registrants only (mostly internal). }) id!: string | number | boolean
ruleRegistry: [],
messageRegistry: {} @Prop({ default: 'text' }) type!: string
@Prop({ required: true }) name: string | boolean
@Prop({ default: false }) value!: any
@Prop({
type: [String, Boolean, Array],
default: false,
}) validation
@Prop({
type: [String, Boolean],
default: false,
}) validationName!: string | boolean
@Prop({
type: Object,
default: () => ({}),
}) validationRules!: Object
@Prop({
type: Object,
default: () => ({}),
}) validationMessages!: Object
@Prop({
type: [Array, String, Boolean],
default: false,
}) errors!: [] | string | boolean
@Prop({
type: String,
default: ERROR_BEHAVIOR.BLUR,
validator: value => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(value)
}) errorBehavior!: string
@Prop({ default: false }) showErrors!: boolean
@Prop({ default: false }) disableErrors!: boolean
@Prop({ default: false }) uploadUrl!: string | boolean
@Prop({ default: 'live' }) uploadBehavior!: string
@Prop({ default: true }) preventWindowDrops!: boolean
defaultId: string = this.$formulario.nextId(this)
localAttributes: Object = {}
localErrors: ValidationError[] = []
proxy: Object = this.getInitialValue()
behavioralErrorVisibility: boolean = this.errorBehavior === 'live'
formShouldShowErrors: boolean = false
validationErrors: [] = []
pendingValidation: Promise = Promise.resolve()
// These registries are used for injected messages registrants only (mostly internal).
ruleRegistry: [] = []
messageRegistry: Object = {}
get context () {
return this.defineModel({
attributes: this.elementAttributes,
blurHandler: this.blurHandler.bind(this),
disableErrors: this.disableErrors,
errors: this.explicitErrors,
allErrors: this.allErrors,
formShouldShowErrors: this.formShouldShowErrors,
getValidationErrors: this.getValidationErrors.bind(this),
hasGivenName: this.hasGivenName,
hasValidationErrors: this.hasValidationErrors.bind(this),
help: this.help,
id: this.id || this.defaultId,
imageBehavior: this.imageBehavior,
limit: this.limit,
name: this.nameOrFallback,
performValidation: this.performValidation.bind(this),
preventWindowDrops: this.preventWindowDrops,
repeatable: this.repeatable,
setErrors: this.setErrors.bind(this),
showValidationErrors: this.showValidationErrors,
uploadBehavior: this.uploadBehavior,
uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulario.getUploader(),
validationErrors: this.validationErrors,
value: this.value,
visibleValidationErrors: this.visibleValidationErrors,
})
}
get parsedValidationRules () {
const parsedValidationRules = {}
Object.keys(this.validationRules).forEach(key => {
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
})
return parsedValidationRules
}
get messages () {
const messages = {}
Object.keys(this.validationMessages).forEach((key) => {
messages[snakeToCamel(key)] = this.validationMessages[key]
})
Object.keys(this.messageRegistry).forEach((key) => {
messages[snakeToCamel(key)] = this.messageRegistry[key]
})
return messages
}
/**
* Reducer for attributes that will be applied to each core input element.
*/
get elementAttributes () {
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) {
computed: { attrs.name = this.name
...context,
parsedValidationRules () {
const parsedValidationRules = {}
Object.keys(this.validationRules).forEach(key => {
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
})
return parsedValidationRules
},
messages () {
const messages = {}
Object.keys(this.validationMessages).forEach((key) => {
messages[snakeToCamel(key)] = this.validationMessages[key]
})
Object.keys(this.messageRegistry).forEach((key) => {
messages[snakeToCamel(key)] = this.messageRegistry[key]
})
return messages
} }
},
watch: { // If there is help text, have this element be described by it.
$attrs: { if (this.help) {
handler (value) { attrs['aria-describedby'] = `${attrs.id}-help`
this.updateLocalAttributes(value)
},
deep: true
},
proxy (newValue, oldValue) {
this.performValidation()
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
formularioValue (newValue, oldValue) {
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
showValidationErrors: {
handler (val) {
this.$emit('error-visibility', val)
},
immediate: true
} }
},
return attrs
}
/**
* Return the elements name, or select a fallback.
*/
get nameOrFallback () {
return this.path !== '' ? `${this.path}.${this.name}` : this.name
}
/**
* Determine if an input has a user-defined name.
*/
get hasGivenName () {
return typeof this.name !== 'boolean'
}
/**
* The validation label to use.
*/
get mergedValidationName () {
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 () {
return this.uploadUrl || this.$formulario.getUploadUrl()
}
/**
* Does this computed property have errors
*/
get hasErrors () {
return this.allErrors.length > 0
}
/**
* Returns if form has actively visible errors (of any kind)
*/
get hasVisibleErrors () {
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
}
/**
* The merged errors computed property.
* Each error is an object with fields message (translated message), rule (rule name) and context
*/
get allErrors (): ValidationError[] {
return [
...this.explicitErrors,
...arrayify(this.validationErrors)
]
}
/**
* All of the currently visible validation errors (does not include error handling)
*/
get visibleValidationErrors () {
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
}
/**
* These are errors we that have been explicity passed to us.
*/
get explicitErrors (): ValidationError[] {
return [
...arrayify(this.errors),
...this.localErrors,
...arrayify(this.error),
].map(message => ({ rule: null, context: null, message }))
}
/**
* Determines if this formulario element is v-modeled or not.
*/
get isVmodeled (): boolean {
return !!(Object.prototype.hasOwnProperty.call(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) {
this.updateLocalAttributes(value)
}
@Watch('proxy')
onProxyChanged (newValue, oldValue) {
this.performValidation()
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
}
@Watch('formularioValue')
onFormularioValueChanged (newValue, oldValue) {
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
}
@Watch('showValidationErrors', { immediate: true })
onShowValidationErrorsChanged (val) {
this.$emit('error-visibility', val)
}
created () { created () {
this.applyInitialValue() this.applyInitialValue()
@ -219,10 +319,8 @@ export default {
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback }) this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
} }
this.updateLocalAttributes(this.$attrs) this.updateLocalAttributes(this.$attrs)
if (this.errorBehavior === 'live') { this.performValidation()
this.performValidation() }
}
},
beforeDestroy () { beforeDestroy () {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') { if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
@ -231,160 +329,203 @@ export default {
if (typeof this.formularioDeregister === 'function') { if (typeof this.formularioDeregister === 'function') {
this.formularioDeregister(this.nameOrFallback) this.formularioDeregister(this.nameOrFallback)
} }
}, }
methods: { /**
getInitialValue () { * Defines the model used throughout the existing context.
if (has(this.$options.propsData, 'value')) { * @param {object} context
return this.value */
} else if (has(this.$options.propsData, 'formularioValue')) { defineModel (context) {
return this.formularioValue return Object.defineProperty(context, 'model', {
} get: this.modelGetter.bind(this),
set: this.modelSetter.bind(this),
})
}
/**
* Get the value from a model.
*/
modelGetter () {
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
if (this[model] === undefined) {
return '' return ''
}, }
return this[model]
}
applyInitialValue () { /**
// This should only be run immediately on created and ensures that the * Set the value from a model.
// proxy and the model are both the same before any additional registration. */
if (!shallowEqualObjects(this.context.model, this.proxy)) { modelSetter (value) {
this.context.model = this.proxy if (!shallowEqualObjects(value, this.proxy)) {
} this.proxy = value
}, }
this.$emit('input', value)
if (this.context.name && typeof this.formularioSetter === 'function') {
this.formularioSetter(this.context.name, value)
}
}
updateLocalAttributes (value) { /**
if (!shallowEqualObjects(value, this.localAttributes)) { * Bound into the context object.
this.localAttributes = value */
} blurHandler () {
}, this.$emit('blur')
if (this.errorBehavior === 'blur') {
this.behavioralErrorVisibility = true
}
}
performValidation () { getInitialValue () {
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules)) if (has(this.$options.propsData, 'value')) {
// Add in ruleRegistry rules. These are added directly via injection from return this.value
// children and not part of the standard validation rule set. } else if (has(this.$options.propsData, 'formularioValue')) {
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules return this.formularioValue
this.pendingValidation = this.runRules(rules) }
.then(messages => this.didValidate(messages)) return ''
return this.pendingValidation }
},
runRules (rules) { applyInitialValue () {
const run = ([rule, args, ruleName, modifier]) => { // This should only be run immediately on created and ensures that the
var res = rule({ // proxy and the model are both the same before any additional registration.
value: this.context.model, if (!shallowEqualObjects(this.context.model, this.proxy)) {
getFormValues: this.getFormValues.bind(this), this.context.model = this.proxy
name: this.context.name }
}, ...args) }
res = (res instanceof Promise) ? res : Promise.resolve(res)
return res.then(result => result ? false : this.getMessageObject(ruleName, args))
}
return new Promise(resolve => { updateLocalAttributes (value) {
const resolveGroups = (groups, allMessages = []) => { if (!shallowEqualObjects(value, this.localAttributes)) {
const ruleGroup = groups.shift() this.localAttributes = value
if (Array.isArray(ruleGroup) && ruleGroup.length) { }
Promise.all(ruleGroup.map(run)) }
.then(messages => messages.filter(m => !!m))
.then(messages => {
messages = Array.isArray(messages) ? messages : []
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
if ((!messages.length || !ruleGroup.bail) && groups.length) {
return resolveGroups(groups, allMessages.concat(messages))
}
return resolve(allMessages.concat(messages))
})
} else {
resolve([])
}
}
resolveGroups(groupBails(rules))
})
},
didValidate (messages) { performValidation () {
const validationChanged = !shallowEqualObjects(messages, this.validationErrors) let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
this.validationErrors = messages // Add in ruleRegistry rules. These are added directly via injection from
if (validationChanged) { // children and not part of the standard validation rule set.
const errorObject = this.getErrorObject() rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
this.$emit('validation', errorObject) this.pendingValidation = this.runRules(rules)
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') { .then(messages => this.didValidate(messages))
this.formularioFieldValidation(errorObject) return this.pendingValidation
} }
}
},
getMessageObject (ruleName, args) { runRules (rules) {
const context = { const run = ([rule, args, ruleName]) => {
args, let res = rule({
name: this.mergedValidationName,
value: this.context.model, value: this.context.model,
vm: this, getFormValues: this.getFormValues.bind(this),
formValues: this.getFormValues() name: this.context.name
} }, ...args)
const message = this.getMessageFunc(ruleName)(context) res = (res instanceof Promise) ? res : Promise.resolve(res)
return res.then(result => result ? false : this.getMessageObject(ruleName, args))
}
return { return new Promise(resolve => {
message: message, const resolveGroups = (groups, allMessages = []) => {
rule: ruleName, const ruleGroup = groups.shift()
context: context if (Array.isArray(ruleGroup) && ruleGroup.length) {
Promise.all(ruleGroup.map(run))
.then(messages => messages.filter(m => !!m))
.then(messages => {
messages = Array.isArray(messages) ? messages : []
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
if ((!messages.length || !ruleGroup.bail) && groups.length) {
return resolveGroups(groups, allMessages.concat(messages))
}
return resolve(allMessages.concat(messages))
})
} else {
resolve([])
}
} }
}, resolveGroups(groupBails(rules))
})
}
getMessageFunc (ruleName) { didValidate (messages) {
ruleName = snakeToCamel(ruleName) const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
if (this.messages && typeof this.messages[ruleName] !== 'undefined') { this.validationErrors = messages
switch (typeof this.messages[ruleName]) { if (validationChanged) {
const errorObject = this.getErrorObject()
this.$emit('validation', errorObject)
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
this.formularioFieldValidation(errorObject)
}
}
}
getMessageObject (ruleName, args) {
const context = {
args,
name: this.mergedValidationName,
value: this.context.model,
vm: this,
formValues: this.getFormValues()
}
const message = this.getMessageFunc(ruleName)(context)
return {
rule: ruleName,
context,
message,
}
}
getMessageFunc (ruleName) {
ruleName = snakeToCamel(ruleName)
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
switch (typeof this.messages[ruleName]) {
case 'function': case 'function':
return this.messages[ruleName] return this.messages[ruleName]
case 'string': case 'string':
case 'boolean': case 'boolean':
return () => this.messages[ruleName] return () => this.messages[ruleName]
}
} }
return (context) => this.$formulario.validationMessage(ruleName, context, this) }
}, return (context) => this.$formulario.validationMessage(ruleName, context, this)
}
hasValidationErrors () { hasValidationErrors () {
return new Promise(resolve => { return new Promise(resolve => {
this.$nextTick(() => { this.$nextTick(() => {
this.pendingValidation.then(() => resolve(!!this.validationErrors.length)) this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
})
}) })
}, })
}
getValidationErrors () { getValidationErrors () {
return new Promise(resolve => { return new Promise(resolve => {
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject()))) this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
}) })
}, }
getErrorObject () { getErrorObject () {
return { return {
name: this.context.nameOrFallback || this.context.name, name: this.context.nameOrFallback || this.context.name,
errors: this.validationErrors.filter(s => typeof s === 'object'), errors: this.validationErrors.filter(s => typeof s === 'object'),
hasErrors: !!this.validationErrors.length hasErrors: !!this.validationErrors.length
}
}
setErrors (errors) {
this.localErrors = arrayify(errors)
}
registerRule (rule, args, ruleName, message = null) {
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
// These are the raw rule format since they will be used directly.
this.ruleRegistry.push([rule, args, ruleName])
if (message !== null) {
this.messageRegistry[ruleName] = message
} }
}, }
}
setErrors (errors) { removeRule (key) {
this.localErrors = arrayify(errors) const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
}, if (ruleIndex >= 0) {
this.ruleRegistry.splice(ruleIndex, 1)
registerRule (rule, args, ruleName, message = null) { delete this.messageRegistry[key]
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
// These are the raw rule format since they will be used directly.
this.ruleRegistry.push([rule, args, ruleName])
if (message !== null) {
this.messageRegistry[ruleName] = message
}
}
},
removeRule (key) {
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
if (ruleIndex >= 0) {
this.ruleRegistry.splice(ruleIndex, 1)
delete this.messageRegistry[key]
}
} }
} }
} }

View File

@ -1,3 +1,7 @@
import { Formulario } from '@/Formulario'
import FormularioInput from '@/FormularioInput.vue'
import { ValidationContext } from '@/validation.types'
/** /**
* This is an object of functions that each produce valid responses. There's no * This is an object of functions that each produce valid responses. There's no
* need for these to be 1-1 with english, feel free to change the wording or * need for these to be 1-1 with english, feel free to change the wording or
@ -17,23 +21,22 @@ const validationMessages = {
/** /**
* The default render method for error messages. * The default render method for error messages.
*/ */
default: function (vm, context) { default (vm: FormularioInput, context: ValidationContext): string {
return vm.$t('validation.default', context) return vm.$t('validation.default', context)
}, },
/** /**
* Valid accepted value. * Valid accepted value.
*/ */
accepted: function (vm, context) { accepted (vm: FormularioInput, 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: function (vm, context) { after (vm: FormularioInput, context: ValidationContext): string {
if (Array.isArray(context.args) && context.args.length) { if (Array.isArray(context.args) && context.args.length) {
context.compare = context.args[0]
return vm.$t('validation.after.compare', context) return vm.$t('validation.after.compare', context)
} }
@ -43,23 +46,22 @@ const validationMessages = {
/** /**
* The value is not a letter. * The value is not a letter.
*/ */
alpha: function (vm, context) { alpha (vm: FormularioInput, context: Object): 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: function (vm, context) { alphanumeric (vm: FormularioInput, context: Object): string {
return vm.$t('validation.alphanumeric', context) return vm.$t('validation.alphanumeric', context)
}, },
/** /**
* The date is not before. * The date is not before.
*/ */
before: function (vm, context) { before (vm: FormularioInput, context: ValidationContext): string {
if (Array.isArray(context.args) && context.args.length) { if (Array.isArray(context.args) && context.args.length) {
context.compare = context.args[0]
return vm.$t('validation.before.compare', context) return vm.$t('validation.before.compare', context)
} }
@ -69,12 +71,10 @@ const validationMessages = {
/** /**
* The value is not between two numbers or lengths * The value is not between two numbers or lengths
*/ */
between: function (vm, context) { between (vm: FormularioInput, context: ValidationContext): string {
context.from = context.args[0]
context.to = context.args[1]
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(value) && force !== 'length') || force === 'value') {
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
return vm.$t('validation.between.force', context) return vm.$t('validation.between.force', context)
} }
@ -84,16 +84,15 @@ const validationMessages = {
/** /**
* The confirmation field does not match * The confirmation field does not match
*/ */
confirm: function (vm, context) { confirm (vm: FormularioInput, 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: function (vm, context) { date (vm: FormularioInput, context: ValidationContext): string {
if (Array.isArray(context.args) && context.args.length) { if (Array.isArray(context.args) && context.args.length) {
context.format = context.args[0]
return vm.$t('validation.date.format', context) return vm.$t('validation.date.format', context)
} }
@ -103,21 +102,21 @@ const validationMessages = {
/** /**
* Is not a valid email address. * Is not a valid email address.
*/ */
email: function (vm, context) { email (vm: FormularioInput, 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: function (vm, context) { endsWith (vm: FormularioInput, 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, context) { in: function (vm: FormularioInput, context: ValidationContext) {
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)
} }
@ -128,32 +127,33 @@ const validationMessages = {
/** /**
* Value is not a match. * Value is not a match.
*/ */
matches: function (vm, context) { matches (vm: FormularioInput, context: ValidationContext) {
return vm.$t('validation.matches.default', context) return vm.$t('validation.matches.default', context)
}, },
/** /**
* The maximum value allowed. * The maximum value allowed.
*/ */
max: function (vm, context) { max (vm: FormularioInput, context: ValidationContext) {
context.maximum = context.args[0] const maximum = context.args[0] as number
if (Array.isArray(context.value)) { if (Array.isArray(context.value)) {
return vm.$tc('validation.max.array', context.maximum, context) return vm.$tc('validation.max.array', maximum, context)
} }
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
if ((!isNaN(context.value) && force !== 'length') || force === 'value') { if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
return vm.$tc('validation.max.force', context.maximum, context) return vm.$tc('validation.max.force', maximum, context)
} }
return vm.$tc('validation.max.default', context.maximum, context) return vm.$tc('validation.max.default', maximum, context)
}, },
/** /**
* The (field-level) error message for mime errors. * The (field-level) error message for mime errors.
*/ */
mime: function (vm, context) { mime (vm: FormularioInput, context: ValidationContext) {
context.types = context.args[0] const types = context.args[0]
if (context.types) {
if (types) {
return vm.$t('validation.mime.default', context) return vm.$t('validation.mime.default', context)
} else { } else {
return vm.$t('validation.mime.no_formats_allowed', context) return vm.$t('validation.mime.no_formats_allowed', context)
@ -163,51 +163,51 @@ const validationMessages = {
/** /**
* The maximum value allowed. * The maximum value allowed.
*/ */
min: function (vm, context) { min (vm: FormularioInput, context: ValidationContext) {
context.minimum = context.args[0] const minimum = context.args[0] as number
if (Array.isArray(context.value)) { if (Array.isArray(context.value)) {
return vm.$tc('validation.min.array', context.minimum, context) return vm.$tc('validation.min.array', minimum, context)
} }
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
if ((!isNaN(context.value) && force !== 'length') || force === 'value') { if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
return vm.$tc('validation.min.force', context.minimum, context) return vm.$tc('validation.min.force', minimum, context)
} }
return vm.$tc('validation.min.default', context.minimum, context) return vm.$tc('validation.min.default', minimum, context)
}, },
/** /**
* The field is not an allowed value * The field is not an allowed value
*/ */
not: function (vm, context) { not (vm: FormularioInput, context: Object) {
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: function (vm, context) { number (vm: FormularioInput, context: Object) {
return vm.$t('validation.number.default', context) return vm.$t('validation.number.default', context)
}, },
/** /**
* Required field. * Required field.
*/ */
required: function (vm, context) { required (vm: FormularioInput, context: Object) {
return vm.$t('validation.required.default', context) return vm.$t('validation.required.default', context)
}, },
/** /**
* Starts with specified value * Starts with specified value
*/ */
startsWith: function (vm, context) { startsWith (vm: FormularioInput, context: Object) {
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: function (vm, context) { url (vm: FormularioInput, context: Object) {
return vm.$t('validation.url.default', context) return vm.$t('validation.url.default', context)
} }
} }
@ -216,8 +216,6 @@ const validationMessages = {
* This creates a vue-formulario plugin that can be imported and used on each * This creates a vue-formulario plugin that can be imported and used on each
* project. * project.
*/ */
export default function (instance) { export default function (instance: Formulario) {
instance.extend({ instance.extend({ validationMessages })
validationMessages
})
} }

5
src/axios.types.ts Normal file
View File

@ -0,0 +1,5 @@
export interface AxiosResponse {
data: any
}
export interface AxiosError {}

2
src/common.types.ts Normal file
View File

@ -0,0 +1,2 @@
export type ArrayType = [any]
export type ObjectType = { [key: string]: any }

View File

@ -1,243 +0,0 @@
import { map, arrayify, shallowEqualObjects } from './utils'
/**
* For a single instance of an input, export all of the context needed to fully
* render that element.
* @return {object}
*/
export default {
context () {
return defineModel.call(this, {
attributes: this.elementAttributes,
blurHandler: blurHandler.bind(this),
disableErrors: this.disableErrors,
errors: this.explicitErrors,
allErrors: this.allErrors,
formShouldShowErrors: this.formShouldShowErrors,
getValidationErrors: this.getValidationErrors.bind(this),
hasGivenName: this.hasGivenName,
hasValidationErrors: this.hasValidationErrors.bind(this),
help: this.help,
id: this.id || this.defaultId,
imageBehavior: this.imageBehavior,
limit: this.limit,
name: this.nameOrFallback,
performValidation: this.performValidation.bind(this),
preventWindowDrops: this.preventWindowDrops,
repeatable: this.repeatable,
setErrors: this.setErrors.bind(this),
showValidationErrors: this.showValidationErrors,
uploadBehavior: this.uploadBehavior,
uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulario.getUploader(),
validationErrors: this.validationErrors,
value: this.value,
visibleValidationErrors: this.visibleValidationErrors,
})
},
// Used in sub-context
nameOrFallback,
hasGivenName,
elementAttributes,
mergedUploadUrl,
// These items are not passed as context
isVmodeled,
mergedValidationName,
explicitErrors,
allErrors,
hasErrors,
hasVisibleErrors,
showValidationErrors,
visibleValidationErrors
}
/**
* Reducer for attributes that will be applied to each core input element.
* @return {object}
*/
function elementAttributes () {
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
}
/**
* The validation label to use.
*/
function mergedValidationName () {
if (this.validationName) {
return this.validationName
}
return this.name
}
/**
* Use the uploadURL on the input if it exists, otherwise use the uploadURL
* that is defined as a plugin option.
*/
function mergedUploadUrl () {
return this.uploadUrl || this.$formulario.getUploadUrl()
}
/**
* Determines if the field should show it's error (if it has one)
* @return {boolean}
*/
function showValidationErrors () {
if (this.showErrors || this.formShouldShowErrors) {
return true
}
return this.behavioralErrorVisibility
}
/**
* All of the currently visible validation errors (does not include error handling)
* @return {array}
*/
function visibleValidationErrors () {
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
}
/**
* Return the elements name, or select a fallback.
*/
function nameOrFallback () {
if (this.path !== '') {
return this.path + '.' + this.name
}
return this.name
}
/**
* determine if an input has a user-defined name
*/
function hasGivenName () {
return typeof this.name !== 'boolean'
}
/**
* Determines if this formulario element is v-modeled or not.
*/
function isVmodeled () {
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
}
/**
* Given an object or array of options, create an array of objects with label,
* value, and id.
* @param {array|object}
* @return {array}
*/
function createOptionList (options) {
if (!Array.isArray(options) && options && typeof options === 'object') {
const optionList = []
const that = this
for (const value in options) {
optionList.push({ value, label: options[value], id: `${that.elementAttributes.id}_${value}` })
}
return optionList
}
return options
}
/**
* These are errors we that have been explicity passed to us.
*/
function explicitErrors () {
let result = arrayify(this.errors)
.concat(this.localErrors)
.concat(arrayify(this.error))
result = result.map(message => ({'message': message, 'rule': null, 'context': null}))
return result;
}
/**
* The merged errors computed property.
* Each error is an object with fields message (translated message), rule (rule name) and context
*/
function allErrors () {
return this.explicitErrors
.concat(arrayify(this.validationErrors))
}
/**
* Does this computed property have errors
*/
function hasErrors () {
return !!this.allErrors.length
}
/**
* Returns if form has actively visible errors (of any kind)
*/
function hasVisibleErrors () {
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
}
/**
* Bound into the context object.
*/
function blurHandler () {
this.$emit('blur')
if (this.errorBehavior === 'blur') {
this.behavioralErrorVisibility = true
}
}
/**
* Defines the model used throughout the existing context.
* @param {object} context
*/
function defineModel (context) {
return Object.defineProperty(context, 'model', {
get: modelGetter.bind(this),
set: modelSetter.bind(this)
})
}
/**
* Get the value from a model.
**/
function modelGetter () {
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
if (this[model] === undefined) {
return ''
}
return this[model]
}
/**
* Set the value from a model.
**/
function modelSetter (value) {
if (!shallowEqualObjects(value, this.proxy)) {
this.proxy = value
}
this.$emit('input', value)
if (this.context.name && typeof this.formularioSetter === 'function') {
this.formularioSetter(this.context.name, value)
}
}

View File

@ -1,3 +1,8 @@
interface UploadedFile {
url: string
name: string
}
/** /**
* A fake uploader used by default. * A fake uploader used by default.
* *
@ -6,8 +11,8 @@
* @param {function} error * @param {function} error
* @param {object} options * @param {object} options
*/ */
export default function (file, progress, error, options) { export default function (file: any, progress: any, error: any, options: any): Promise<UploadedFile> {
return new Promise((resolve, reject) => { return new Promise(resolve => {
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random()) const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
const start = performance.now() const start = performance.now()

View File

@ -1,11 +0,0 @@
/**
* The default backend error handler assumes a failed axios instance. You can
* easily override this function with fetch. The expected output is defined
* on the documentation site vueformulate.com.
*/
export default function (err) {
if (typeof err === 'object' && err.response) {
}
return {}
}

View File

@ -5,10 +5,11 @@
* overly terse for that reason alone, we wouldn't necessarily recommend this. * overly terse for that reason alone, we wouldn't necessarily recommend this.
*/ */
const fi = 'FormularioInput' const fi = 'FormularioInput'
const add = (n, c) => ({ const add = (classification: string, c?: string) => ({
classification: n, classification,
component: fi + (c || (n[0].toUpperCase() + n.substr(1))) component: fi + (c || (classification[0].toUpperCase() + classification.substr(1)))
}) })
export default { export default {
// === SINGLE LINE TEXT STYLE INPUTS // === SINGLE LINE TEXT STYLE INPUTS
...[ ...[

View File

@ -1,10 +0,0 @@
const i = 'image/'
export default {
csv: 'text/csv',
gif: i + 'gif',
jpg: i + 'jpeg',
jpeg: i + 'jpeg',
png: i + 'png',
pdf: 'application/pdf',
svg: i + 'svg+xml'
}

9
src/libs/mimes.ts Normal file
View File

@ -0,0 +1,9 @@
export default {
csv: 'text/csv',
gif: 'image/gif',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
png: 'image/png',
pdf: 'application/pdf',
svg: 'image/svg+xml'
}

View File

@ -1,251 +0,0 @@
import { shallowEqualObjects, has, getNested, setNested } from './utils'
/**
* Component registry with inherent depth to handle complex nesting. This is
* important for features such as grouped fields.
*/
class Registry {
/**
* Create a new registry of components.
* @param {vm} ctx The host vm context of the registry.
*/
constructor (ctx) {
this.registry = new Map()
this.ctx = ctx
}
/**
* Add an item to the registry.
* @param {string|array} name
* @param {vue} component
*/
add (name, component) {
this.registry.set(name, component)
return this
}
/**
* Remove an item from the registry.
* @param {string} name
*/
remove (name) {
this.registry.delete(name)
const { [name]: value, ...newProxy } = this.ctx.proxy
this.ctx.proxy = newProxy
return this
}
/**
* Check if the registry has the given key.
* @param {string|array} key
*/
has (key) {
return this.registry.has(key)
}
/**
* Get a particular registry value.
* @param {string} key
*/
get (key) {
return this.registry.get(key)
}
/**
* Map over the registry (recursively).
* @param {function} callback
*/
map (callback) {
const value = {}
this.registry.forEach((component, field) => Object.assign(value, { [field]: callback(component, field) }))
return value
}
/**
* Return the keys of the registry.
*/
keys () {
return Array.from(this.registry.keys())
}
/**
* Fully register a component.
* @param {string} field name of the field.
* @param {vm} component the actual component instance.
*/
register (field, component) {
if (this.registry.has(field)) {
return false
}
this.registry.set(field, component)
const hasVModelValue = has(component.$options.propsData, 'formularioValue')
const hasValue = has(component.$options.propsData, 'value')
if (
!hasVModelValue &&
this.ctx.hasInitialValue &&
getNested(this.ctx.initialValues, field) !== undefined
) {
// In the case that the form is carrying an initial value and the
// element is not, set it directly.
component.context.model = getNested(this.ctx.initialValues, field)
} else if (
(hasVModelValue || hasValue) &&
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
) {
// In this case, the field is v-modeled or has an initial value and the
// form has no value or a different value, so use the field value
this.ctx.setFieldValue(field, component.proxy)
}
if (this.childrenShouldShowErrors) {
component.formShouldShowErrors = true
}
}
/**
* Reduce the registry.
* @param {function} callback
* @param accumulator
*/
reduce (callback, accumulator) {
this.registry.forEach((component, field) => {
accumulator = callback(accumulator, component, field)
})
return accumulator
}
/**
* Data props to expose.
*/
dataProps () {
return {
proxy: {},
registry: this,
register: this.register.bind(this),
deregister: field => this.remove(field),
childrenShouldShowErrors: false
}
}
}
/**
* The context component.
* @param {component} contextComponent
*/
export default function useRegistry (contextComponent) {
const registry = new Registry(contextComponent)
return registry.dataProps()
}
/**
* Computed properties related to the registry.
*/
export function useRegistryComputed () {
return {
hasInitialValue () {
return (
(this.formularioValue && typeof this.formularioValue === 'object') ||
(this.values && typeof this.values === 'object') ||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
)
},
isVmodeled () {
return !!(Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formularioValue') &&
this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
},
initialValues () {
if (
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
return Object.assign({}, 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 {}
}
}
}
/**
* Methods used in the registry.
*/
export function useRegistryMethods (without = []) {
const methods = {
applyInitialValues () {
if (this.hasInitialValue) {
this.proxy = this.initialValues
}
},
setFieldValue (field, value) {
if (value === undefined) {
const { [field]: value, ...proxy } = this.proxy
this.proxy = proxy
} else {
setNested(this.proxy, field, value)
}
this.$emit('input', Object.assign({}, this.proxy))
},
getFormValues () {
return this.proxy
},
hasValidationErrors () {
return Promise.all(this.registry.reduce((resolvers, cmp, name) => {
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
return resolvers
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
},
showErrors () {
this.childrenShouldShowErrors = true
this.registry.map(input => {
input.formShouldShowErrors = true
})
},
hideErrors () {
this.childrenShouldShowErrors = false
this.registry.map(input => {
input.formShouldShowErrors = false
input.behavioralErrorVisibility = false
})
},
setValues (values) {
// Collect all keys, existing and incoming
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
keys.forEach(field => {
if (this.registry.has(field) &&
!shallowEqualObjects(getNested(values, field), getNested(this.proxy, field)) &&
!shallowEqualObjects(getNested(values, field), this.registry.get(field).proxy)
) {
this.setFieldValue(field, getNested(values, field))
this.registry.get(field).context.model = getNested(values, field)
}
})
this.applyInitialValues()
}
}
return Object.keys(methods).reduce((withMethods, key) => {
return without.includes(key) ? withMethods : { ...withMethods, [key]: methods[key] }
}, {})
}
/**
* Providers related to the registry.
*/
export function useRegistryProviders (ctx) {
return {
formularioSetter: ctx.setFieldValue,
formularioRegister: ctx.register,
formularioDeregister: ctx.deregister,
getFormValues: ctx.getFormValues
}
}

126
src/libs/registry.ts Normal file
View File

@ -0,0 +1,126 @@
import { shallowEqualObjects, has, getNested } from './utils'
import { ObjectType } from '@/common.types'
import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue'
/**
* Component registry with inherent depth to handle complex nesting. This is
* important for features such as grouped fields.
*/
export default class Registry {
public ctx: FormularioForm
private registry: Map<string, FormularioForm>
/**
* Create a new registry of components.
* @param {FormularioForm} ctx The host vm context of the registry.
*/
constructor (ctx: FormularioForm) {
this.registry = new Map()
this.ctx = ctx
}
/**
* Add an item to the registry.
*/
add (name: string, component: FormularioForm) {
this.registry.set(name, component)
return this
}
/**
* Remove an item from the registry.
* @param {string} name
*/
remove (name: string) {
this.registry.delete(name)
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [name]: value, ...newProxy } = this.ctx.proxy
// @ts-ignore
this.ctx.proxy = newProxy
return this
}
/**
* Check if the registry has the given key.
*/
has (key: string) {
return this.registry.has(key)
}
/**
* Get a particular registry value.
*/
get (key: string): FormularioForm | undefined {
return this.registry.get(key)
}
/**
* Map over the registry (recursively).
*/
map (callback: Function) {
const value = {}
this.registry.forEach((component, field) => Object.assign(value, { [field]: callback(component, field) }))
return value
}
/**
* Return the keys of the registry.
*/
keys (): string[] {
return Array.from(this.registry.keys())
}
/**
* Fully register a component.
* @param {string} field name of the field.
* @param {FormularioForm} component the actual component instance.
*/
register (field: string, component: FormularioInput) {
if (this.registry.has(field)) {
return false
}
this.registry.set(field, component)
const hasVModelValue = has(component.$options.propsData as ObjectType, 'formularioValue')
const hasValue = has(component.$options.propsData as ObjectType, 'value')
if (
!hasVModelValue &&
// @ts-ignore
this.ctx.hasInitialValue &&
// @ts-ignore
getNested(this.ctx.initialValues, field) !== undefined
) {
// In the case that the form is carrying an initial value and the
// element is not, set it directly.
// @ts-ignore
component.context.model = getNested(this.ctx.initialValues, field)
} else if (
(hasVModelValue || hasValue) &&
// @ts-ignore
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
) {
// In this case, the field is v-modeled or has an initial value and the
// form has no value or a different value, so use the field value
// @ts-ignore
this.ctx.setFieldValue(field, component.proxy)
}
// @ts-ignore
if (this.ctx.childrenShouldShowErrors) {
// @ts-ignore
component.formShouldShowErrors = true
}
}
/**
* Reduce the registry.
* @param {function} callback
* @param accumulator
*/
reduce (callback: Function, accumulator: any) {
this.registry.forEach((component, field) => {
accumulator = callback(accumulator, component, field)
})
return accumulator
}
}

View File

@ -1,6 +1,8 @@
// @ts-ignore
import isUrl from 'is-url' import isUrl from 'is-url'
import FileUpload from '../FileUpload' import FileUpload from '../FileUpload'
import { shallowEqualObjects, regexForFormat } from './utils' import { shallowEqualObjects, regexForFormat, has } from './utils'
import { ObjectType } from '@/common.types'
/** /**
* Library of rules * Library of rules
@ -9,15 +11,15 @@ export default {
/** /**
* Rule: the value must be "yes", "on", "1", or true * Rule: the value must be "yes", "on", "1", or true
*/ */
accepted: function ({ value }) { accepted ({ value }: { value: any }) {
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value)) return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
}, },
/** /**
* Rule: checks if a value is after a given date. Defaults to current time * Rule: checks if a value is after a given date. Defaults to current time
*/ */
after: function ({ value }, compare = false) { after ({ value }: { value: string }, compare: string | false = false) {
const timestamp = Date.parse(compare || new Date()) const timestamp = compare !== false ? Date.parse(compare) : new Date()
const fieldValue = Date.parse(value) const fieldValue = Date.parse(value)
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp)) return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
}, },
@ -25,32 +27,34 @@ export default {
/** /**
* Rule: checks if the value is only alpha * Rule: checks if the value is only alpha
*/ */
alpha: function ({ value }, set = 'default') { alpha ({ value }: { value: string }, set: string = 'default') {
const sets = { const sets = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/, default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z]+$/ latin: /^[a-zA-Z]+$/
} }
const selectedSet = sets.hasOwnProperty(set) ? set : 'default' const selectedSet = has(sets, set) ? set : 'default'
// @ts-ignore
return Promise.resolve(sets[selectedSet].test(value)) return Promise.resolve(sets[selectedSet].test(value))
}, },
/** /**
* Rule: checks if the value is alpha numeric * Rule: checks if the value is alpha numeric
*/ */
alphanumeric: function ({ value }, set = 'default') { alphanumeric ({ value }: { value: string }, set = 'default') {
const sets = { const sets = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/, default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]+$/ latin: /^[a-zA-Z0-9]+$/
} }
const selectedSet = sets.hasOwnProperty(set) ? set : 'default' const selectedSet = has(sets, set) ? set : 'default'
// @ts-ignore
return Promise.resolve(sets[selectedSet].test(value)) return Promise.resolve(sets[selectedSet].test(value))
}, },
/** /**
* Rule: checks if a value is after a given date. Defaults to current time * Rule: checks if a value is after a given date. Defaults to current time
*/ */
before: function ({ value }, compare = false) { before ({ value }: { value: string }, compare: string | false = false) {
const timestamp = Date.parse(compare || new Date()) const timestamp = compare !== false ? Date.parse(compare) : new Date()
const fieldValue = Date.parse(value) const fieldValue = Date.parse(value)
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp)) return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
}, },
@ -58,19 +62,19 @@ export default {
/** /**
* Rule: checks if the value is between two other values * Rule: checks if the value is between two other values
*/ */
between: function ({ value }, from = 0, to = 10, force) { between ({ value }: { value: string | number }, from: number = 0, to: number = 10, force: string) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (from === null || to === null || isNaN(from) || isNaN(to)) { if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false return false
} }
if ((!isNaN(value) && force !== 'length') || force === 'value') { if ((!isNaN(Number(value)) && force !== 'length') || force === 'value') {
value = Number(value) value = Number(value)
from = Number(from) from = Number(from)
to = Number(to) to = Number(to)
return (value > from && value < to) return (value > from && value < to)
} }
if (typeof value === 'string' || force === 'length') { if (typeof value === 'string' || force === 'length') {
value = !isNaN(value) ? value.toString() : value value = (!isNaN(Number(value)) ? value.toString() : value) as string
return value.length > from && value.length < to return value.length > from && value.length < to
} }
return false return false
@ -81,10 +85,10 @@ 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: function ({ value, getFormValues, name }, field) { confirm ({ value, getFormValues, name }: { value: any, getFormValues: () => ObjectType, name: string }, field: string) {
return Promise.resolve((() => { return Promise.resolve((() => {
const formValues = getFormValues() const formValues = getFormValues()
var 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`
} }
@ -96,9 +100,9 @@ export default {
* Rule: ensures the value is a date according to Date.parse(), or a format * Rule: ensures the value is a date according to Date.parse(), or a format
* regex. * regex.
*/ */
date: function ({ value }, format = false) { date ({ value }: { value: string }, format: string | false = false) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (format && typeof format === 'string') { if (format) {
return regexForFormat(format).test(value) return regexForFormat(format).test(value)
} }
return !isNaN(Date.parse(value)) return !isNaN(Date.parse(value))
@ -108,7 +112,7 @@ export default {
/** /**
* Rule: tests * Rule: tests
*/ */
email: function ({ value }) { email ({ value }: { value: string}) {
if (!value) { if (!value) {
return Promise.resolve(() => { return true }) return Promise.resolve(() => { return true })
} }
@ -121,7 +125,7 @@ export default {
/** /**
* Rule: Value ends with one of the given Strings * Rule: Value ends with one of the given Strings
*/ */
endsWith: function ({ value }, ...stack) { endsWith: function ({ value }: any, ...stack: any[]) {
if (!value) { if (!value) {
return Promise.resolve(() => { return true }) return Promise.resolve(() => { return true })
} }
@ -141,7 +145,7 @@ export default {
/** /**
* Rule: Value is in an array (stack). * Rule: Value is in an array (stack).
*/ */
in: function ({ value }, ...stack) { in: function ({ value }: any, ...stack: any[]) {
return Promise.resolve(stack.find(item => { return Promise.resolve(stack.find(item => {
if (typeof item === 'object') { if (typeof item === 'object') {
return shallowEqualObjects(item, value) return shallowEqualObjects(item, value)
@ -153,7 +157,7 @@ export default {
/** /**
* Rule: Match the value against a (stack) of patterns or strings * Rule: Match the value against a (stack) of patterns or strings
*/ */
matches: function ({ value }, ...stack) { matches: function ({ value }: any, ...stack: any[]) {
return Promise.resolve(!!stack.find(pattern => { return Promise.resolve(!!stack.find(pattern => {
if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') { if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') {
pattern = new RegExp(pattern.substr(1, pattern.length - 2)) pattern = new RegExp(pattern.substr(1, pattern.length - 2))
@ -168,7 +172,7 @@ export default {
/** /**
* Check the file type is correct. * Check the file type is correct.
*/ */
mime: function ({ value }, ...types) { mime: function ({ value }: any, ...types: string[]) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (value instanceof FileUpload) { if (value instanceof FileUpload) {
const fileList = value.getFiles() const fileList = value.getFiles()
@ -186,7 +190,7 @@ export default {
/** /**
* Check the minimum value of a particular. * Check the minimum value of a particular.
*/ */
min: function ({ value }, minimum = 1, force) { min: function ({ value }: any, minimum = 1, force: string) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
minimum = !isNaN(minimum) ? Number(minimum) : minimum minimum = !isNaN(minimum) ? Number(minimum) : minimum
@ -207,7 +211,7 @@ export default {
/** /**
* Check the maximum value of a particular. * Check the maximum value of a particular.
*/ */
max: function ({ value }, maximum = 10, force) { max: function ({ value }: any, maximum = 10, force: string) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
maximum = !isNaN(maximum) ? Number(maximum) : maximum maximum = !isNaN(maximum) ? Number(maximum) : maximum
@ -228,7 +232,7 @@ export default {
/** /**
* Rule: Value is not in stack. * Rule: Value is not in stack.
*/ */
not: function ({ value }, ...stack) { not: function ({ value }: any, ...stack: any[]) {
return Promise.resolve(stack.find(item => { return Promise.resolve(stack.find(item => {
if (typeof item === 'object') { if (typeof item === 'object') {
return shallowEqualObjects(item, value) return shallowEqualObjects(item, value)
@ -240,16 +244,16 @@ export default {
/** /**
* Rule: checks if the value is only alpha numeric * Rule: checks if the value is only alpha numeric
*/ */
number: function ({ value }) { number ({ value }: { value: any }) {
return Promise.resolve(!isNaN(value)) return Promise.resolve(!isNaN(value))
}, },
/** /**
* Rule: must be a value * Rule: must be a value
*/ */
required: function ({ value }, isRequired = true) { required ({ value }: any, isRequired: string|boolean = true) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (!isRequired || ['no', 'false'].includes(isRequired)) { if (!isRequired || ['no', 'false'].includes(isRequired as string)) {
return true return true
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -271,16 +275,14 @@ export default {
/** /**
* Rule: Value starts with one of the given Strings * Rule: Value starts with one of the given Strings
*/ */
startsWith: function ({ value }, ...stack) { startsWith ({ value }: { value: any }, ...stack: any[]) {
if (!value) { if (!value) {
return Promise.resolve(() => { return true }) return Promise.resolve(() => { return true })
} }
return Promise.resolve((() => { return Promise.resolve((() => {
if (typeof value === 'string' && stack.length) { if (typeof value === 'string' && stack.length) {
return stack.find(item => { return stack.find(item => value.startsWith(item)) !== undefined
return value.startsWith(item)
}) !== undefined
} else if (typeof value === 'string' && stack.length === 0) { } else if (typeof value === 'string' && stack.length === 0) {
return true return true
} }
@ -291,14 +293,14 @@ export default {
/** /**
* Rule: checks if a string is a valid url * Rule: checks if a string is a valid url
*/ */
url: function ({ value }) { url ({ value }: { value: string }) {
return Promise.resolve(isUrl(value)) return Promise.resolve(isUrl(value))
}, },
/** /**
* Rule: not a true rule more like a compiler flag. * Rule: not a true rule more like a compiler flag.
*/ */
bail: function () { bail () {
return Promise.resolve(true) return Promise.resolve(true)
} }
} }

View File

@ -1,48 +1,42 @@
import FileUpload from '../FileUpload' import FileUpload from '../FileUpload'
type ArrayType = [any]
type ObjectType = { [key: string]: any }
/** /**
* Function to map over an object. * Function to map over an object.
* @param {Object} obj An object to map over * @param {Object} original An object to map over
* @param {Function} callback * @param {Function} callback
*/ */
export function map (original, callback) { export function map (original: ObjectType, callback: Function) {
const obj = {} const obj: ObjectType = {}
for (let key in original) { for (const key in original) {
obj[key] = callback(key, original[key]) if (Object.prototype.hasOwnProperty.call(original, key)) {
obj[key] = callback(key, original[key])
}
} }
return obj return obj
} }
/** export function shallowEqualObjects (objA: any, objB: any) {
* Shallow equal.
* @param {} objA
* @param {*} objB
*/
export function shallowEqualObjects (objA, objB) {
if (objA === objB) { if (objA === objB) {
return true return true
} }
if (!objA || !objB) { if (!objA || !objB) {
return false return false
} }
var aKeys = Object.keys(objA)
var bKeys = Object.keys(objB) const aKeys = Object.keys(objA)
var len = aKeys.length const bKeys = Object.keys(objB)
const len = aKeys.length
if (bKeys.length !== len) { if (bKeys.length !== len) {
return false return false
} }
if (objA instanceof Date && objB instanceof Date) { for (let i = 0; i < len; i++) {
return objA.getTime() === objB.getTime(); const key = aKeys[i]
}
if (len === 0) {
return objA === objB;
}
for (var i = 0; i < len; i++) {
var key = aKeys[i]
if (objA[key] !== objB[key]) { if (objA[key] !== objB[key]) {
return false return false
@ -55,7 +49,7 @@ export function shallowEqualObjects (objA, objB) {
* Given a string, convert snake_case to camelCase * Given a string, convert snake_case to camelCase
* @param {String} string * @param {String} string
*/ */
export function snakeToCamel (string) { export function snakeToCamel (string: string | any) {
if (typeof string === 'string') { if (typeof string === 'string') {
return string.replace(/([_][a-z0-9])/ig, ($1) => { return string.replace(/([_][a-z0-9])/ig, ($1) => {
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') { if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
@ -68,10 +62,11 @@ export function snakeToCamel (string) {
} }
/** /**
* Given a string, object, falsey, or array - return an array. * Converts to array.
* @param {mixed} item * If given parameter is not string, object ot array, result will be an empty array.
* @param {*} item
*/ */
export function arrayify (item) { export function arrayify (item: any) {
if (!item) { if (!item) {
return [] return []
} }
@ -93,7 +88,7 @@ export function arrayify (item) {
* @param {array} rules and array of functions * @param {array} rules and array of functions
* @return {array} an array of functions * @return {array} an array of functions
*/ */
export function parseRules (validation, rules) { export function parseRules (validation: any, rules: any): any[] {
if (typeof validation === 'string') { if (typeof validation === 'string') {
return parseRules(validation.split('|'), rules) return parseRules(validation.split('|'), rules)
} }
@ -106,41 +101,42 @@ export function parseRules (validation, rules) {
/** /**
* Given a string or function, parse it and return an array in the format * Given a string or function, parse it and return an array in the format
* [fn, [...arguments]] * [fn, [...arguments]]
* @param {string|function} rule
*/ */
function parseRule (rule, rules) { function parseRule (rule: any, rules: ObjectType) {
if (typeof rule === 'function') { if (typeof rule === 'function') {
return [rule, []] return [rule, []]
} }
if (Array.isArray(rule) && rule.length) { if (Array.isArray(rule) && rule.length) {
rule = rule.map(r => r) // light clone rule = rule.slice() // light clone
const [ruleName, modifier] = parseModifier(rule.shift()) const [ruleName, modifier] = parseModifier(rule.shift())
if (typeof ruleName === 'string' && rules.hasOwnProperty(ruleName)) { if (typeof ruleName === 'string' && Object.prototype.hasOwnProperty.call(rules, ruleName)) {
return [rules[ruleName], rule, ruleName, modifier] return [rules[ruleName], rule, ruleName, modifier]
} }
if (typeof ruleName === 'function') { if (typeof ruleName === 'function') {
return [ruleName, rule, ruleName, modifier] return [ruleName, rule, ruleName, modifier]
} }
} }
if (typeof rule === 'string') { if (typeof rule === 'string') {
const segments = rule.split(':') const segments = rule.split(':')
const [ruleName, modifier] = parseModifier(segments.shift()) const [ruleName, modifier] = parseModifier(segments.shift())
if (rules.hasOwnProperty(ruleName)) {
if (Object.prototype.hasOwnProperty.call(rules, ruleName)) {
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier] return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
} else { } else {
throw new Error(`Unknown validation rule ${rule}`) throw new Error(`Unknown validation rule ${rule}`)
} }
} }
return false return false
} }
/** /**
* Return the rule name with the applicable modifier as an array. * Return the rule name with the applicable modifier as an array.
* @param {string} ruleName
* @return {array} [ruleName, modifier]
*/ */
function parseModifier (ruleName) { function parseModifier (ruleName: any): [string|any, string|null] {
if (/^[\^]/.test(ruleName.charAt(0))) { if (typeof ruleName === 'string' && /^[\^]/.test(ruleName.charAt(0))) {
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)] return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
} }
return [snakeToCamel(ruleName), null] return [snakeToCamel(ruleName), null]
@ -159,7 +155,7 @@ function parseModifier (ruleName) {
* [[required, min, max]] * [[required, min, max]]
* @param {array} rules * @param {array} rules
*/ */
export function groupBails (rules) { export function groupBails (rules: any[]) {
const groups = [] const groups = []
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail') const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
if (bailIndex >= 0) { if (bailIndex >= 0) {
@ -174,11 +170,13 @@ export function groupBails (rules) {
} }
return groups.reduce((groups, group) => { return groups.reduce((groups, group) => {
// @ts-ignore
const splitByMod = (group, bailGroup = false) => { const splitByMod = (group, bailGroup = false) => {
if (group.length < 2) { if (group.length < 2) {
return Object.defineProperty([group], 'bail', { value: bailGroup }) return Object.defineProperty([group], 'bail', { value: bailGroup })
} }
const splits = [] const splits = []
// @ts-ignore
const modIndex = group.findIndex(([,,, modifier]) => modifier === '^') const modIndex = group.findIndex(([,,, modifier]) => modifier === '^')
if (modIndex >= 0) { if (modIndex >= 0) {
const preMod = group.splice(0, modIndex) const preMod = group.splice(0, modIndex)
@ -200,7 +198,7 @@ export function groupBails (rules) {
* Escape a string for use in regular expressions. * Escape a string for use in regular expressions.
* @param {string} string * @param {string} string
*/ */
export function escapeRegExp (string) { export function escapeRegExp (string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
} }
@ -208,8 +206,8 @@ export function escapeRegExp (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 * @param {string} format
*/ */
export function regexForFormat (format) { export function regexForFormat (format: string) {
let escaped = `^${escapeRegExp(format)}$` const escaped = `^${escapeRegExp(format)}$`
const formats = { const formats = {
MM: '(0[1-9]|1[012])', MM: '(0[1-9]|1[012])',
M: '([1-9]|1[012])', M: '([1-9]|1[012])',
@ -219,15 +217,16 @@ export function regexForFormat (format) {
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))
} }
/** /**
* Check if * Check if
* @param {mixed} data * @param {*} data
*/ */
export function isValueType (data) { export function isScalar (data: any) {
switch (typeof data) { switch (typeof data) {
case 'symbol': case 'symbol':
case 'number': case 'number':
@ -236,10 +235,7 @@ export function isValueType (data) {
case 'undefined': case 'undefined':
return true return true
default: default:
if (data === null) { return data === null
return true
}
return false
} }
} }
@ -247,29 +243,33 @@ export function isValueType (data) {
* A simple (somewhat non-comprehensive) cloneDeep function, valid for our use * A simple (somewhat non-comprehensive) cloneDeep function, valid for our use
* case of needing to unbind reactive watchers. * case of needing to unbind reactive watchers.
*/ */
export function cloneDeep (obj) { export function cloneDeep (value: any) {
if (typeof obj !== 'object') { if (typeof value !== 'object') {
return obj return value
} }
const isArr = Array.isArray(obj)
const newObj = isArr ? [] : {} const copy: ArrayType | ObjectType = Array.isArray(value) ? [] : {}
for (const key in obj) {
if (obj[key] instanceof FileUpload || isValueType(obj[key])) { for (const key in value) {
newObj[key] = obj[key] if (Object.prototype.hasOwnProperty.call(value, key)) {
} else { if (isScalar(value[key]) || value[key] instanceof FileUpload) {
newObj[key] = cloneDeep(obj[key]) copy[key] = value[key]
} else {
copy[key] = cloneDeep(value[key])
}
} }
} }
return newObj
return copy
} }
/** /**
* Given a locale string, parse the options. * Given a locale string, parse the options.
* @param {string} locale * @param {string} locale
*/ */
export function parseLocale (locale) { export function parseLocale (locale: string) {
const segments = locale.split('-') const segments = locale.split('-')
return segments.reduce((options, segment) => { return segments.reduce((options: string[], segment: string) => {
if (options.length) { if (options.length) {
options.unshift(`${options[0]}-${segment}`) options.unshift(`${options[0]}-${segment}`)
} }
@ -280,75 +280,74 @@ export function parseLocale (locale) {
/** /**
* Shorthand for Object.prototype.hasOwnProperty.call (space saving) * Shorthand for Object.prototype.hasOwnProperty.call (space saving)
*/ */
export function has (ctx, prop) { export function has (ctx: ObjectType, prop: string): boolean {
return Object.prototype.hasOwnProperty.call(ctx, prop) return Object.prototype.hasOwnProperty.call(ctx, prop)
} }
/** /**
* Set a unique Symbol identifier on an object. * Set a unique Symbol identifier on an object.
* @param {object} o
* @param {Symbol} id
*/ */
export function setId (o, id) { export function setId (o: object, id: Symbol) {
return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') })) return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') }))
} }
export function getNested(obj, field) { export function getNested (obj: ObjectType, field: string) {
let fieldParts = field.split('.'); const fieldParts = field.split('.')
let result: ObjectType = obj
let result = obj;
for (const key in fieldParts) { for (const key in fieldParts) {
let matches = fieldParts[key].match(/(.+)\[(\d+)\]$/); const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
if (result === undefined) { if (result === undefined) {
return null return null
} }
if (matches) { if (matches) {
result = result[matches[1]]; result = result[matches[1]]
if (result === undefined) { if (result === undefined) {
return null return null
} }
result = result[matches[2]]; result = result[matches[2]]
} else { } else {
result = result[fieldParts[key]]; result = result[fieldParts[key]]
} }
} }
return result; return result
} }
export function setNested(obj, field, value) { export function setNested (obj: ObjectType, field: string, value: any) {
let fieldParts = field.split('.'); const fieldParts = field.split('.')
let subProxy = obj; let subProxy: ObjectType = obj
for (let i = 0; i < fieldParts.length; i++) { for (let i = 0; i < fieldParts.length; i++) {
let fieldPart = fieldParts[i]; const fieldPart = fieldParts[i]
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
let matches = fieldPart.match(/(.+)\[(\d+)\]$/);
if (matches) { if (matches) {
if (subProxy[matches[1]] === undefined) { if (subProxy[matches[1]] === undefined) {
subProxy[matches[1]] = []; subProxy[matches[1]] = []
} }
subProxy = subProxy[matches[1]]; subProxy = subProxy[matches[1]]
if (i == fieldParts.length - 1) { if (i === fieldParts.length - 1) {
subProxy[matches[2]] = value subProxy[matches[2]] = value
break; break
} else { } else {
subProxy = subProxy[matches[2]]; subProxy = subProxy[matches[2]]
} }
} else { } else {
if (i == fieldParts.length - 1) { if (i === fieldParts.length - 1) {
subProxy[fieldPart] = value subProxy[fieldPart] = value
break; break
} else { } else {
// eslint-disable-next-line max-depth
if (subProxy[fieldPart] === undefined) { if (subProxy[fieldPart] === undefined) {
subProxy[fieldPart] = {}; subProxy[fieldPart] = {}
} }
subProxy = subProxy[fieldPart]; subProxy = subProxy[fieldPart]
} }
} }
} }
return obj; return obj
} }

21
src/shims-ext.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import Vue from 'vue'
import { Formulario } from '@/Formulario'
declare module 'vue/types/vue' {
interface VueRoute {
path: string
}
interface Vue {
$formulario: Formulario,
$route: VueRoute,
$t: Function,
$tc: Function,
}
interface FormularioForm extends Vue {
name: string | boolean
proxy: Object
hasValidationErrors(): Promise<boolean>
}
}

13
src/shims-tsx.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}

4
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

14
src/validation.types.ts Normal file
View File

@ -0,0 +1,14 @@
interface ValidationContext {
args: any[]
name: string
value: any
}
interface ValidationError {
rule?: string
context?: any
message: string
}
export { ValidationContext }
export { ValidationError }

View File

@ -1,24 +1,25 @@
const path = require('path') const path = require('path')
module.exports = { module.exports = {
rootDir: path.resolve(__dirname, '../'), rootDir: path.resolve(__dirname, '../'),
moduleFileExtensions: [ moduleFileExtensions: [
'js', 'js',
'json', 'json',
'vue' 'ts',
], 'vue',
moduleNameMapper: { ],
'^@/(.*)$': '<rootDir>/src/$1' moduleNameMapper: {
}, '^@/(.*)$': '<rootDir>/src/$1'
modulePaths: [ },
"<rootDir>" modulePaths: [
], "<rootDir>"
transform: { ],
'.*\\.js$': '<rootDir>/node_modules/babel-jest', transform: {
'.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor' '.*\\.js$': '<rootDir>/node_modules/babel-jest',
}, '.*\\.ts$': '<rootDir>/node_modules/ts-jest',
collectCoverageFrom: [ '.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor'
"src/*.{js,vue}", },
], collectCoverageFrom: [
// verbose: true "src/*.{js,vue}",
],
} }

View File

@ -1,4 +1,4 @@
import Formulario from '@/Formulario.js' import Formulario from '@/Formulario.ts'
describe('Formulario', () => { describe('Formulario', () => {
it('can merge simple object', () => { it('can merge simple object', () => {
@ -71,7 +71,7 @@ describe('Formulario', () => {
Vue.component = function (name, instance) { Vue.component = function (name, instance) {
registry.push(name) registry.push(name)
} }
Formulario.install(Vue, { extended: true }) Formulario.install(Vue)
expect(Vue.prototype.$formulario).toBe(Formulario) expect(Vue.prototype.$formulario).toBe(Formulario)
expect(registry).toEqual(components) expect(registry).toEqual(components)
}) })

View File

@ -1,10 +1,9 @@
import Vue from 'vue' import Vue from 'vue'
import { mount, shallowMount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import Formulario from '../../src/Formulario.js' import FormSubmission from '@/FormSubmission.ts'
import FormSubmission from '../../src/FormSubmission.js' import Formulario from '@/Formulario.ts'
import FormularioForm from '@/FormularioForm.vue' import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue'
function validationMessages (instance) { function validationMessages (instance) {
instance.extend({ instance.extend({
@ -387,10 +386,10 @@ describe('FormularioForm', () => {
it('displays field errors on inputs with errors prop', async () => { it('displays field errors on inputs with errors prop', async () => {
const wrapper = mount(FormularioForm, { const wrapper = mount(FormularioForm, {
propsData: { errors: { sipple: ['This field has an error'] }}, propsData: { errors: { fieldWithErrors: ['This field has an error'] }},
slots: { slots: {
default: ` default: `
<FormularioInput v-slot="vSlot" name="sipple"> <FormularioInput v-slot="vSlot" name="fieldWithErrors">
<span v-for="error in vSlot.context.allErrors">{{ error.message }}</span> <span v-for="error in vSlot.context.allErrors">{{ error.message }}</span>
</FormularioInput> </FormularioInput>
` `

View File

@ -1,8 +1,7 @@
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 Formulario from '@/Formulario.js' import Formulario from '@/Formulario.ts'
import FormularioInput from '@/FormularioInput.vue'
import FormularioForm from '@/FormularioForm.vue' import FormularioForm from '@/FormularioForm.vue'
import FormularioGrouping from '@/FormularioGrouping.vue' import FormularioGrouping from '@/FormularioGrouping.vue'

View File

@ -1,11 +1,12 @@
import Vue from 'vue'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { mount, createLocalVue } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Formulario from '@/Formulario.js' import Formulario from '@/Formulario.ts'
import FormularioForm from '@/FormularioForm.vue' import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue' import FormularioInput from '@/FormularioInput.vue'
const globalRule = jest.fn((context) => { return false }) const globalRule = jest.fn(() => { return false })
function validationMessages (instance) { function validationMessages (instance) {
instance.extend({ instance.extend({
@ -18,19 +19,14 @@ function validationMessages (instance) {
}) })
} }
const localVue = createLocalVue() Vue.use(Formulario, {
localVue.use(Formulario, {
plugins: [validationMessages], plugins: [validationMessages],
rules: { rules: { globalRule }
globalRule
}
}) })
describe('FormularioInput', () => { describe('FormularioInput', () => {
it('allows custom field-rule level validation strings', async () => { it('allows custom field-rule level validation strings', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|in:abcdef', validation: 'required|in:abcdef',
@ -64,7 +60,6 @@ describe('FormularioInput', () => {
it('allows custom field-rule level validation functions', async () => { it('allows custom field-rule level validation functions', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|in:abcdef', validation: 'required|in:abcdef',
@ -82,7 +77,6 @@ describe('FormularioInput', () => {
it('uses custom async validation rules on defined on the field', async () => { it('uses custom async validation rules on defined on the field', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|foobar', validation: 'required|foobar',
@ -105,7 +99,6 @@ describe('FormularioInput', () => {
it('uses custom sync validation rules on defined on the field', async () => { it('uses custom sync validation rules on defined on the field', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|foobar', validation: 'required|foobar',
@ -128,7 +121,6 @@ describe('FormularioInput', () => {
it('uses global custom validation rules', async () => { it('uses global custom validation rules', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|globalRule', validation: 'required|globalRule',
@ -142,7 +134,6 @@ describe('FormularioInput', () => {
it('emits correct validation event', async () => { it('emits correct validation event', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
validation: 'required', validation: 'required',
errorBehavior: 'live', errorBehavior: 'live',
@ -167,7 +158,6 @@ describe('FormularioInput', () => {
it('emits a error-visibility event on blur', async () => { it('emits a error-visibility event on blur', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
validation: 'required', validation: 'required',
errorBehavior: 'blur', errorBehavior: 'blur',
@ -187,7 +177,6 @@ describe('FormularioInput', () => {
it('emits error-visibility event immediately when live', async () => { it('emits error-visibility event immediately when live', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
validation: 'required', validation: 'required',
errorBehavior: 'live', errorBehavior: 'live',
@ -201,7 +190,6 @@ describe('FormularioInput', () => {
it('Does not emit an error-visibility event if visibility did not change', async () => { it('Does not emit an error-visibility event if visibility did not change', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
validation: 'in:xyz', validation: 'in:xyz',
errorBehavior: 'live', errorBehavior: 'live',
@ -221,7 +209,6 @@ describe('FormularioInput', () => {
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, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' } propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
}) })
await flushPromises(); await flushPromises();
@ -230,7 +217,6 @@ describe('FormularioInput', () => {
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 () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' } propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
}) })
await flushPromises(); await flushPromises();
@ -239,7 +225,6 @@ describe('FormularioInput', () => {
it('can avoid bail behavior by using modifier', async () => { it('can avoid bail behavior by using modifier', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
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();
@ -248,7 +233,6 @@ describe('FormularioInput', () => {
it('prevents later error messages when modified rule fails', async () => { it('prevents later error messages when modified rule fails', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
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();
@ -257,7 +241,6 @@ describe('FormularioInput', () => {
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 () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
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();
@ -266,7 +249,6 @@ describe('FormularioInput', () => {
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 () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
localVue,
propsData: { propsData: {
validation: 'required', validation: 'required',
errorBehavior: 'submit', errorBehavior: 'submit',
@ -291,8 +273,7 @@ describe('FormularioInput', () => {
it('displays errors when error-behavior is submit and form is submitted', async () => { it('displays errors when error-behavior is submit and form is submitted', async () => {
const wrapper = mount(FormularioForm, { const wrapper = mount(FormularioForm, {
localVue, propsData: { name: 'test' },
propsData: {name: 'test'},
slots: { slots: {
default: ` default: `
<FormularioInput v-slot="inputProps" validation="required" name="testinput" error-behavior="submit"> <FormularioInput v-slot="inputProps" validation="required" name="testinput" error-behavior="submit">

View File

@ -1,4 +1,4 @@
import { parseRules, parseLocale, regexForFormat, cloneDeep, isValueType, snakeToCamel, groupBails } from '@/libs/utils' import { parseRules, parseLocale, regexForFormat, cloneDeep, isScalar, snakeToCamel, groupBails } from '@/libs/utils'
import rules from '@/libs/rules' import rules from '@/libs/rules'
import FileUpload from '@/FileUpload'; import FileUpload from '@/FileUpload';
@ -99,22 +99,22 @@ describe('regexForFormat', () => {
it('fails date like YYYY-MM-DD with out of bounds day', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false)) it('fails date like YYYY-MM-DD with out of bounds day', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false))
}) })
describe('isValueType', () => { describe('isScalar', () => {
it('passes on strings', () => expect(isValueType('hello')).toBe(true)) it('passes on strings', () => expect(isScalar('hello')).toBe(true))
it('passes on numbers', () => expect(isValueType(123)).toBe(true)) it('passes on numbers', () => expect(isScalar(123)).toBe(true))
it('passes on booleans', () => expect(isValueType(false)).toBe(true)) it('passes on booleans', () => expect(isScalar(false)).toBe(true))
it('passes on symbols', () => expect(isValueType(Symbol(123))).toBe(true)) it('passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
it('passes on null', () => expect(isValueType(null)).toBe(true)) it('passes on null', () => expect(isScalar(null)).toBe(true))
it('passes on undefined', () => expect(isValueType(undefined)).toBe(true)) it('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
it('fails on pojo', () => expect(isValueType({})).toBe(false)) it('fails on pojo', () => expect(isScalar({})).toBe(false))
it('fails on custom type', () => expect(isValueType(FileUpload)).toBe(false)) it('fails on custom type', () => expect(isScalar(FileUpload)).toBe(false))
}) })
describe('cloneDeep', () => { describe('cloneDeep', () => {

40
tsconfig.json Normal file
View File

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

2118
yarn.lock

File diff suppressed because it is too large Load Diff