Library renamed to formulario
This commit is contained in:
parent
8651cd9d30
commit
83d36526c3
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -10,5 +10,5 @@ assignees: ''
|
||||
**Describe the new feature you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**What percentage of vue-formulate users would benefit?**
|
||||
**What percentage of vue-formulario users would benefit?**
|
||||
80%
|
||||
|
@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 RetailDriver LLC
|
||||
Copyright (c) 2020 Braid LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
@ -5,16 +5,15 @@ import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
export default {
|
||||
input: 'src/Formulate.js', // Path relative to package.json
|
||||
input: 'src/Formulario.js', // Path relative to package.json
|
||||
output: [
|
||||
{
|
||||
name: 'Formulate',
|
||||
name: 'Formulario',
|
||||
exports: 'default',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
'is-url': 'isUrl',
|
||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
||||
},
|
||||
sourcemap: false
|
||||
}
|
||||
|
@ -6,16 +6,15 @@ import internal from 'rollup-plugin-internal'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
export default {
|
||||
input: 'src/Formulate.js', // Path relative to package.json
|
||||
input: 'src/Formulario.js', // Path relative to package.json
|
||||
output: {
|
||||
name: 'VueFormulate',
|
||||
name: 'VueFormulario',
|
||||
exports: 'default',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
'is-url': 'isUrl',
|
||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@ -24,7 +23,7 @@ export default {
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs(),
|
||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url', '@braid/vue-formulate-i18n']),
|
||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url']),
|
||||
vue({
|
||||
css: true, // Dynamically inject css as a <style> tag
|
||||
compileTemplate: true // Explicitly convert template to render function
|
||||
|
173
package.json
173
package.json
@ -1,89 +1,88 @@
|
||||
{
|
||||
"name": "@braid/vue-formulate",
|
||||
"version": "2.3.0",
|
||||
"description": "The easiest way to build forms in Vue.",
|
||||
"main": "dist/formulate.umd.js",
|
||||
"module": "dist/formulate.esm.js",
|
||||
"unpkg": "dist/formulate.min.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browser": {
|
||||
"./sfc": "src/Formulate.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulate.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulate.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulate.min.js | wc -c)b gzip\"",
|
||||
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulate.esm.js",
|
||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulate.umd.js",
|
||||
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulate.min.js",
|
||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
|
||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
||||
"build:size": "gzip -c dist/formulate.esm.js | wc -c",
|
||||
"dev": "vue-cli-service serve --port=7872 examples/main.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/wearebraid/vue-formulate.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"form",
|
||||
"forms",
|
||||
"validation",
|
||||
"vuex",
|
||||
"validate"
|
||||
],
|
||||
"author": "Justin Schroeder <justin@wearebraid.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wearebraid/vue-formulate/issues"
|
||||
},
|
||||
"homepage": "https://www.vueformulate.com",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/test-utils": "^1.0.2",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.5.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"flush-promises": "^1.0.2",
|
||||
"jest": "^25.5.4",
|
||||
"jest-vue-preprocessor": "^1.7.1",
|
||||
"rollup": "^1.32.1",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-internal": "^1.0.4",
|
||||
"rollup-plugin-multi-input": "^1.1.1",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-vue": "^5.1.7",
|
||||
"typescript": "^3.9.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-runtime-helpers": "^1.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue-template-es2015-compiler": "^1.9.1",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate-i18n": "^1.6.2",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^2.1.11"
|
||||
}
|
||||
"name": "@retailcrm/vue-formulario",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/formulario.umd.js",
|
||||
"module": "dist/formulario.esm.js",
|
||||
"unpkg": "dist/formulario.min.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browser": {
|
||||
"./sfc": "src/Formulario.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulario.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulario.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulario.min.js | wc -c)b gzip\"",
|
||||
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js",
|
||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
|
||||
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js",
|
||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
|
||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
||||
"build:size": "gzip -c dist/formulario.esm.js | wc -c",
|
||||
"dev": "vue-cli-service serve --port=7872 examples/main.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"form",
|
||||
"forms",
|
||||
"validation",
|
||||
"validate"
|
||||
],
|
||||
"author": "RetailDriverLLC <integration@retailcrm.ru>",
|
||||
"contributors": [
|
||||
"Justin Schroeder <justin@wearebraid.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/retailcrm/vue-formulario/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/test-utils": "^1.0.2",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.5.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"flush-promises": "^1.0.2",
|
||||
"jest": "^25.5.4",
|
||||
"jest-vue-preprocessor": "^1.7.1",
|
||||
"rollup": "^1.32.1",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-internal": "^1.0.4",
|
||||
"rollup-plugin-multi-input": "^1.1.1",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-vue": "^5.1.7",
|
||||
"typescript": "^3.9.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-runtime-helpers": "^1.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue-template-es2015-compiler": "^1.9.1",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-plain-object": "^3.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^2.1.11"
|
||||
}
|
||||
}
|
||||
|
@ -5,210 +5,210 @@ import nanoid from 'nanoid/non-secure'
|
||||
* the upload flow.
|
||||
*/
|
||||
class FileUpload {
|
||||
/**
|
||||
* Create a file upload object.
|
||||
* @param {FileList} fileList
|
||||
* @param {object} context
|
||||
*/
|
||||
constructor (input, context, options = {}) {
|
||||
this.input = input
|
||||
this.fileList = input.files
|
||||
this.files = []
|
||||
this.options = { ...{ mimes: {} }, ...options }
|
||||
this.results = false
|
||||
this.context = context
|
||||
if (Array.isArray(this.fileList)) {
|
||||
this.rehydrateFileList(this.fileList)
|
||||
} else {
|
||||
this.addFileList(this.fileList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a pre-existing array of files, create a faux FileList.
|
||||
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
|
||||
* @param {string} pathKey the object-key to access the url (defaults to "url")
|
||||
*/
|
||||
rehydrateFileList (items) {
|
||||
const fauxFileList = items.reduce((fileList, item) => {
|
||||
const key = this.options ? this.options.fileUrlKey : 'url'
|
||||
const url = item[key]
|
||||
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
|
||||
const mime = this.options.mimes[ext] || false
|
||||
fileList.push(Object.assign({}, item, url ? {
|
||||
name: url.substr((url.lastIndexOf('/') + 1) || 0),
|
||||
type: item.type ? item.type : mime,
|
||||
previewData: url
|
||||
} : {}))
|
||||
return fileList
|
||||
}, [])
|
||||
this.results = items
|
||||
this.addFileList(fauxFileList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce an array of files and alert the callback.
|
||||
* @param {FileList}
|
||||
*/
|
||||
addFileList (fileList) {
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i]
|
||||
const uuid = nanoid()
|
||||
const removeFile = function () {
|
||||
this.removeFile(uuid)
|
||||
}
|
||||
this.files.push({
|
||||
progress: false,
|
||||
error: false,
|
||||
complete: false,
|
||||
justFinished: false,
|
||||
name: file.name || 'file-upload',
|
||||
file,
|
||||
uuid,
|
||||
path: false,
|
||||
removeFile: removeFile.bind(this),
|
||||
previewData: file.previewData || false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file has an.
|
||||
*/
|
||||
hasUploader () {
|
||||
return !!this.context.uploader
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given uploader is axios instance. This isn't a great way of
|
||||
* testing if it is or not, but AFIK there isn't a better way right now:
|
||||
*
|
||||
* https://github.com/axios/axios/issues/737
|
||||
*/
|
||||
uploaderIsAxios () {
|
||||
if (
|
||||
this.hasUploader &&
|
||||
typeof this.context.uploader.request === 'function' &&
|
||||
typeof this.context.uploader.get === 'function' &&
|
||||
typeof this.context.uploader.delete === 'function' &&
|
||||
typeof this.context.uploader.post === 'function'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new uploader function.
|
||||
*/
|
||||
getUploader (...args) {
|
||||
if (this.uploaderIsAxios()) {
|
||||
const formData = new FormData()
|
||||
formData.append(this.context.name || 'file', args[0])
|
||||
if (this.context.uploadUrl === false) {
|
||||
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
|
||||
}
|
||||
return this.context.uploader.post(this.context.uploadUrl, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: progressEvent => {
|
||||
// args[1] here is the upload progress handler function
|
||||
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
|
||||
/**
|
||||
* Create a file upload object.
|
||||
* @param {FileList} fileList
|
||||
* @param {object} context
|
||||
*/
|
||||
constructor (input, context, options = {}) {
|
||||
this.input = input
|
||||
this.fileList = input.files
|
||||
this.files = []
|
||||
this.options = { ...{ mimes: {} }, ...options }
|
||||
this.results = false
|
||||
this.context = context
|
||||
if (Array.isArray(this.fileList)) {
|
||||
this.rehydrateFileList(this.fileList)
|
||||
} else {
|
||||
this.addFileList(this.fileList)
|
||||
}
|
||||
})
|
||||
.then(res => res.data)
|
||||
.catch(err => args[2](err))
|
||||
}
|
||||
return this.context.uploader(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the file upload.
|
||||
*/
|
||||
upload () {
|
||||
if (this.results) {
|
||||
return Promise.resolve(this.results)
|
||||
/**
|
||||
* Given a pre-existing array of files, create a faux FileList.
|
||||
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
|
||||
* @param {string} pathKey the object-key to access the url (defaults to "url")
|
||||
*/
|
||||
rehydrateFileList (items) {
|
||||
const fauxFileList = items.reduce((fileList, item) => {
|
||||
const key = this.options ? this.options.fileUrlKey : 'url'
|
||||
const url = item[key]
|
||||
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
|
||||
const mime = this.options.mimes[ext] || false
|
||||
fileList.push(Object.assign({}, item, url ? {
|
||||
name: url.substr((url.lastIndexOf('/') + 1) || 0),
|
||||
type: item.type ? item.type : mime,
|
||||
previewData: url
|
||||
} : {}))
|
||||
return fileList
|
||||
}, [])
|
||||
this.results = items
|
||||
this.addFileList(fauxFileList)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.hasUploader) {
|
||||
return reject(new Error('No uploader has been defined'))
|
||||
}
|
||||
Promise.all(this.files.map(file => {
|
||||
return file.path ? Promise.resolve(file.path) : this.getUploader(
|
||||
file.file,
|
||||
(progress) => {
|
||||
file.progress = progress
|
||||
if (progress >= 100) {
|
||||
if (!file.complete) {
|
||||
file.justFinished = true
|
||||
setTimeout(() => { file.justFinished = false }, this.options.uploadJustCompleteDuration)
|
||||
}
|
||||
file.complete = true
|
||||
|
||||
/**
|
||||
* Produce an array of files and alert the callback.
|
||||
* @param {FileList}
|
||||
*/
|
||||
addFileList (fileList) {
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i]
|
||||
const uuid = nanoid()
|
||||
const removeFile = function () {
|
||||
this.removeFile(uuid)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
file.progress = 0
|
||||
file.error = error
|
||||
file.complete = true
|
||||
},
|
||||
this.options
|
||||
)
|
||||
}))
|
||||
.then(results => {
|
||||
this.results = results
|
||||
resolve(results)
|
||||
})
|
||||
.catch(err => { throw new Error(err) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file from the uploader (and the file list)
|
||||
* @param {string} uuid
|
||||
*/
|
||||
removeFile (uuid) {
|
||||
this.files = this.files.filter(file => file.uuid !== uuid)
|
||||
this.context.performValidation()
|
||||
if (window && this.fileList instanceof FileList) {
|
||||
const transfer = new DataTransfer()
|
||||
this.files.map(file => transfer.items.add(file.file))
|
||||
this.fileList = transfer.files
|
||||
this.input.files = this.fileList
|
||||
this.files.push({
|
||||
progress: false,
|
||||
error: false,
|
||||
complete: false,
|
||||
justFinished: false,
|
||||
name: file.name || 'file-upload',
|
||||
file,
|
||||
uuid,
|
||||
path: false,
|
||||
removeFile: removeFile.bind(this),
|
||||
previewData: file.previewData || false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* load image previews for all uploads.
|
||||
*/
|
||||
loadPreviews () {
|
||||
this.files.map(file => {
|
||||
if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => Object.assign(file, { previewData: e.target.result })
|
||||
reader.readAsDataURL(file.file)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Check if the file has an.
|
||||
*/
|
||||
hasUploader () {
|
||||
return !!this.context.uploader
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files.
|
||||
*/
|
||||
getFileList () {
|
||||
return this.fileList
|
||||
}
|
||||
/**
|
||||
* Check if the given uploader is axios instance. This isn't a great way of
|
||||
* testing if it is or not, but AFIK there isn't a better way right now:
|
||||
*
|
||||
* https://github.com/axios/axios/issues/737
|
||||
*/
|
||||
uploaderIsAxios () {
|
||||
if (
|
||||
this.hasUploader &&
|
||||
typeof this.context.uploader.request === 'function' &&
|
||||
typeof this.context.uploader.get === 'function' &&
|
||||
typeof this.context.uploader.delete === 'function' &&
|
||||
typeof this.context.uploader.post === 'function'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files.
|
||||
*/
|
||||
getFiles () {
|
||||
return this.files
|
||||
}
|
||||
/**
|
||||
* Get a new uploader function.
|
||||
*/
|
||||
getUploader (...args) {
|
||||
if (this.uploaderIsAxios()) {
|
||||
const formData = new FormData()
|
||||
formData.append(this.context.name || 'file', args[0])
|
||||
if (this.context.uploadUrl === false) {
|
||||
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
|
||||
}
|
||||
return this.context.uploader.post(this.context.uploadUrl, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: progressEvent => {
|
||||
// args[1] here is the upload progress handler function
|
||||
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
|
||||
}
|
||||
})
|
||||
.then(res => res.data)
|
||||
.catch(err => args[2](err))
|
||||
}
|
||||
return this.context.uploader(...args)
|
||||
}
|
||||
|
||||
toString () {
|
||||
const descriptor = this.files.length ? this.files.length + ' files' : 'empty'
|
||||
return this.results ? JSON.stringify(this.results, null, ' ') : `FileUpload(${descriptor})`
|
||||
}
|
||||
/**
|
||||
* Perform the file upload.
|
||||
*/
|
||||
upload () {
|
||||
if (this.results) {
|
||||
return Promise.resolve(this.results)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.hasUploader) {
|
||||
return reject(new Error('No uploader has been defined'))
|
||||
}
|
||||
Promise.all(this.files.map(file => {
|
||||
return file.path ? Promise.resolve(file.path) : this.getUploader(
|
||||
file.file,
|
||||
(progress) => {
|
||||
file.progress = progress
|
||||
if (progress >= 100) {
|
||||
if (!file.complete) {
|
||||
file.justFinished = true
|
||||
setTimeout(() => { file.justFinished = false }, this.options.uploadJustCompleteDuration)
|
||||
}
|
||||
file.complete = true
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
file.progress = 0
|
||||
file.error = error
|
||||
file.complete = true
|
||||
},
|
||||
this.options
|
||||
)
|
||||
}))
|
||||
.then(results => {
|
||||
this.results = results
|
||||
resolve(results)
|
||||
})
|
||||
.catch(err => { throw new Error(err) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file from the uploader (and the file list)
|
||||
* @param {string} uuid
|
||||
*/
|
||||
removeFile (uuid) {
|
||||
this.files = this.files.filter(file => file.uuid !== uuid)
|
||||
this.context.performValidation()
|
||||
if (window && this.fileList instanceof FileList) {
|
||||
const transfer = new DataTransfer()
|
||||
this.files.map(file => transfer.items.add(file.file))
|
||||
this.fileList = transfer.files
|
||||
this.input.files = this.fileList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* load image previews for all uploads.
|
||||
*/
|
||||
loadPreviews () {
|
||||
this.files.map(file => {
|
||||
if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => Object.assign(file, { previewData: e.target.result })
|
||||
reader.readAsDataURL(file.file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files.
|
||||
*/
|
||||
getFileList () {
|
||||
return this.fileList
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files.
|
||||
*/
|
||||
getFiles () {
|
||||
return this.files
|
||||
}
|
||||
|
||||
toString () {
|
||||
const descriptor = this.files.length ? this.files.length + ' files' : 'empty'
|
||||
return this.results ? JSON.stringify(this.results, null, ' ') : `FileUpload(${descriptor})`
|
||||
}
|
||||
}
|
||||
|
||||
export default FileUpload
|
||||
|
@ -2,41 +2,41 @@ import { cloneDeep } from './libs/utils'
|
||||
import FileUpload from './FileUpload'
|
||||
|
||||
export default class FormSubmission {
|
||||
/**
|
||||
* Initialize a formulate form.
|
||||
* @param {vm} form an instance of FormulateForm
|
||||
*/
|
||||
constructor (form) {
|
||||
this.form = form
|
||||
}
|
||||
/**
|
||||
* Initialize a formulario form.
|
||||
* @param {vm} form an instance of FormularioForm
|
||||
*/
|
||||
constructor (form) {
|
||||
this.form = form
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the form has any validation errors.
|
||||
*
|
||||
* @return {Promise} resolves a boolean
|
||||
*/
|
||||
hasValidationErrors () {
|
||||
return this.form.hasValidationErrors()
|
||||
}
|
||||
/**
|
||||
* Determine if the form has any validation errors.
|
||||
*
|
||||
* @return {Promise} resolves a boolean
|
||||
*/
|
||||
hasValidationErrors () {
|
||||
return this.form.hasValidationErrors()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously generate the values payload of this form.
|
||||
* @return {Promise} resolves to json
|
||||
*/
|
||||
values () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pending = []
|
||||
const values = cloneDeep(this.form.proxy)
|
||||
for (const key in values) {
|
||||
if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) {
|
||||
pending.push(
|
||||
this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
||||
)
|
||||
}
|
||||
}
|
||||
Promise.all(pending)
|
||||
.then(() => resolve(values))
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Asynchronously generate the values payload of this form.
|
||||
* @return {Promise} resolves to json
|
||||
*/
|
||||
values () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pending = []
|
||||
const values = cloneDeep(this.form.proxy)
|
||||
for (const key in values) {
|
||||
if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) {
|
||||
pending.push(
|
||||
this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
||||
)
|
||||
}
|
||||
}
|
||||
Promise.all(pending)
|
||||
.then(() => resolve(values))
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
276
src/Formulario.js
Normal file
276
src/Formulario.js
Normal file
@ -0,0 +1,276 @@
|
||||
import library from './libs/library'
|
||||
import rules from './libs/rules'
|
||||
import mimes from './libs/mimes'
|
||||
import FileUpload from './FileUpload'
|
||||
import { arrayify, parseLocale, has } from './libs/utils'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import fauxUploader from './libs/faux-uploader'
|
||||
import FormularioForm from './FormularioForm.vue'
|
||||
import FormularioInput from './FormularioInput.vue'
|
||||
import FormularioGrouping from './FormularioGrouping.vue'
|
||||
|
||||
/**
|
||||
* The base formulario library.
|
||||
*/
|
||||
class Formulario {
|
||||
/**
|
||||
* Instantiate our base options.
|
||||
*/
|
||||
constructor () {
|
||||
this.options = {}
|
||||
this.defaults = {
|
||||
components: {
|
||||
FormularioForm,
|
||||
FormularioInput,
|
||||
FormularioGrouping,
|
||||
},
|
||||
library,
|
||||
rules,
|
||||
mimes,
|
||||
locale: false,
|
||||
uploader: fauxUploader,
|
||||
uploadUrl: false,
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
errorHandler: (err) => err,
|
||||
plugins: [],
|
||||
idPrefix: 'formulario-'
|
||||
}
|
||||
this.registry = new Map()
|
||||
this.idRegistry = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install vue formulario, and register it’s components.
|
||||
*/
|
||||
install (Vue, options) {
|
||||
Vue.prototype.$formulario = this
|
||||
this.options = this.defaults
|
||||
var plugins = this.defaults.plugins
|
||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
||||
plugins = plugins.concat(options.plugins)
|
||||
}
|
||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||
this.extend(options || {})
|
||||
for (var componentName in this.options.components) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a deterministically generated id based on the sequence by which it
|
||||
* was requested. This should be *theoretically* the same SSR as client side.
|
||||
* However, SSR and deterministic ids can be very challenging, so this
|
||||
* implementation is open to community review.
|
||||
*/
|
||||
nextId (vm) {
|
||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
||||
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
|
||||
this.idRegistry[pathPrefix] = 0
|
||||
}
|
||||
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of options, apply them to the pre-existing options.
|
||||
* @param {Object} extendWith
|
||||
*/
|
||||
extend (extendWith) {
|
||||
if (typeof extendWith === 'object') {
|
||||
this.options = this.merge(this.options, extendWith)
|
||||
return this
|
||||
}
|
||||
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object by copying properties of base and mergeWith.
|
||||
* Note: arrays don't overwrite - they push
|
||||
*
|
||||
* @param {Object} base
|
||||
* @param {Object} mergeWith
|
||||
* @param {boolean} concatArrays
|
||||
*/
|
||||
merge (base, mergeWith, concatArrays = true) {
|
||||
var merged = {}
|
||||
for (var key in base) {
|
||||
if (mergeWith.hasOwnProperty(key)) {
|
||||
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
|
||||
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
|
||||
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
|
||||
merged[key] = base[key].concat(mergeWith[key])
|
||||
} else {
|
||||
merged[key] = mergeWith[key]
|
||||
}
|
||||
} else {
|
||||
merged[key] = base[key]
|
||||
}
|
||||
}
|
||||
for (var prop in mergeWith) {
|
||||
if (!merged.hasOwnProperty(prop)) {
|
||||
merged[prop] = mergeWith[prop]
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what "class" of input this element is given the "type".
|
||||
* @param {string} type
|
||||
*/
|
||||
classify (type) {
|
||||
if (this.options.library.hasOwnProperty(type)) {
|
||||
return this.options.library[type].classification
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what type of component to render given the "type".
|
||||
* @param {string} type
|
||||
*/
|
||||
component (type) {
|
||||
if (this.options.library.hasOwnProperty(type)) {
|
||||
return this.options.library[type].component
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules by merging any passed in with global rules.
|
||||
* @return {object} object of validation functions
|
||||
*/
|
||||
rules (rules = {}) {
|
||||
return { ...this.options.rules, ...rules }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get the vue-i18n configured locale.
|
||||
*/
|
||||
i18n (vm) {
|
||||
if (vm.$i18n) {
|
||||
switch (typeof vm.$i18n.locale) {
|
||||
case 'string':
|
||||
return vm.$i18n.locale
|
||||
case 'function':
|
||||
return vm.$i18n.locale()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation message for a particular error.
|
||||
*/
|
||||
validationMessage (rule, validationContext, vm) {
|
||||
return rule
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a FormularioForm register it.
|
||||
* @param {vm} form
|
||||
*/
|
||||
register (form) {
|
||||
if (form.$options.name === 'FormularioForm' && form.name) {
|
||||
this.registry.set(form.name, form)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a form, remove it from the registry.
|
||||
* @param {vm} form
|
||||
*/
|
||||
deregister (form) {
|
||||
if (
|
||||
form.$options.name === 'FormularioForm' &&
|
||||
form.name &&
|
||||
this.registry.has(form.name)
|
||||
) {
|
||||
this.registry.delete(form.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array, this function will attempt to make sense of the given error
|
||||
* and hydrate a form with the resulting errors.
|
||||
*
|
||||
* @param {error} err
|
||||
* @param {string} formName
|
||||
* @param {error}
|
||||
*/
|
||||
handle (err, formName, skip = false) {
|
||||
const e = skip ? err : this.options.errorHandler(err, formName)
|
||||
if (formName && this.registry.has(formName)) {
|
||||
this.registry.get(formName).applyErrors({
|
||||
formErrors: arrayify(e.formErrors),
|
||||
inputErrors: e.inputErrors || {}
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a form.
|
||||
* @param {string} formName
|
||||
* @param {object} initialValue
|
||||
*/
|
||||
reset (formName, initialValue = {}) {
|
||||
this.resetValidation(formName)
|
||||
this.setValues(formName, initialValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form's validation messages.
|
||||
* @param {string} formName
|
||||
*/
|
||||
resetValidation (formName) {
|
||||
const form = this.registry.get(formName)
|
||||
form.hideErrors(formName)
|
||||
form.namedErrors = []
|
||||
form.namedFieldErrors = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the form values.
|
||||
* @param {string} formName
|
||||
* @param {object} values
|
||||
*/
|
||||
setValues (formName, values) {
|
||||
if (values && !Array.isArray(values) && typeof values === 'object') {
|
||||
const form = this.registry.get(formName)
|
||||
form.setValues({ ...values })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file uploader.
|
||||
*/
|
||||
getUploader () {
|
||||
return this.options.uploader || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global upload url.
|
||||
*/
|
||||
getUploadUrl () {
|
||||
return this.options.uploadUrl || false
|
||||
}
|
||||
|
||||
/**
|
||||
* When re-hydrating a file uploader with an array, get the sub-object key to
|
||||
* access the url of the file. Usually this is just "url".
|
||||
*/
|
||||
getFileUrlKey () {
|
||||
return this.options.fileUrlKey || 'url'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of an upload.
|
||||
*/
|
||||
createUpload (fileList, context) {
|
||||
return new FileUpload(fileList, context, this.options)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Formulario()
|
85
src/FormularioFiles.vue
Normal file
85
src/FormularioFiles.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="fileUploads.length"
|
||||
class="formulario-files"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileUploads"
|
||||
:key="file.uuid"
|
||||
:data-has-error="!!file.error"
|
||||
:data-has-preview="!!(imagePreview && file.previewData)"
|
||||
>
|
||||
<div class="formulario-file">
|
||||
<div
|
||||
v-if="!!(imagePreview && file.previewData)"
|
||||
class="formulario-file-image-preview"
|
||||
>
|
||||
<img
|
||||
:src="file.previewData"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="formulario-file-name"
|
||||
:title="file.name"
|
||||
v-text="file.name"
|
||||
/>
|
||||
<div
|
||||
v-if="file.progress !== false"
|
||||
:data-just-finished="file.justFinished"
|
||||
:data-is-finished="!file.justFinished && file.complete"
|
||||
class="formulario-file-progress"
|
||||
>
|
||||
<div
|
||||
class="formulario-file-progress-inner"
|
||||
:style="{width: file.progress + '%'}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="(file.complete && !file.justFinished) || file.progress === false"
|
||||
class="formulario-file-remove"
|
||||
@click="file.removeFile"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="file.error"
|
||||
class="formulario-file-upload-error"
|
||||
v-text="file.error"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUpload from './FileUpload'
|
||||
|
||||
export default {
|
||||
name: 'FormularioFiles',
|
||||
props: {
|
||||
files: {
|
||||
type: FileUpload,
|
||||
required: true
|
||||
},
|
||||
imagePreview: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fileUploads () {
|
||||
return this.files.files || []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
files () {
|
||||
if (this.imagePreview) {
|
||||
this.files.loadPreviews()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.imagePreview) {
|
||||
this.files.loadPreviews()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
167
src/FormularioForm.vue
Normal file
167
src/FormularioForm.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<form
|
||||
:class="classes"
|
||||
@submit.prevent="formSubmitted"
|
||||
>
|
||||
<slot :errors="mergedFormErrors" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { arrayify, has } from './libs/utils'
|
||||
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||
import FormSubmission from './FormSubmission'
|
||||
|
||||
export default {
|
||||
provide () {
|
||||
return {
|
||||
...useRegistryProviders(this),
|
||||
observeErrors: this.addErrorObserver,
|
||||
removeErrorObserver: this.removeErrorObserver,
|
||||
formularioFieldValidation: this.formularioFieldValidation,
|
||||
path: ''
|
||||
}
|
||||
},
|
||||
name: 'FormularioForm',
|
||||
model: {
|
||||
prop: 'formularioValue',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
formularioValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
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 () {
|
||||
const classes = { 'formulario-form': true }
|
||||
if (this.name) {
|
||||
classes[`formulario-form--${this.name}`] = true
|
||||
}
|
||||
return classes
|
||||
},
|
||||
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: {
|
||||
formularioValue: {
|
||||
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
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$formulario.register(this)
|
||||
this.applyInitialValues()
|
||||
},
|
||||
destroyed () {
|
||||
this.$formulario.deregister(this)
|
||||
},
|
||||
methods: {
|
||||
...useRegistryMethods(),
|
||||
applyErrors ({ formErrors, inputErrors }) {
|
||||
// given an object of errors, apply them to this form
|
||||
this.namedErrors = formErrors
|
||||
this.namedFieldErrors = inputErrors
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
43
src/FormularioGrouping.vue
Normal file
43
src/FormularioGrouping.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulario-group"
|
||||
data-type="group"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormularioGrouping',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isArrayItem: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
path: this.groupPath
|
||||
}
|
||||
},
|
||||
inject: ['path'],
|
||||
computed: {
|
||||
groupPath () {
|
||||
if (this.isArrayItem) {
|
||||
return this.path + '[' + this.name + ']';
|
||||
} else {
|
||||
if (this.path === '') {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
return this.path + '.' + this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
331
src/FormularioInput.vue
Normal file
331
src/FormularioInput.vue
Normal file
@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulario-input"
|
||||
:data-has-errors="hasErrors"
|
||||
:data-is-showing-errors="hasVisibleErrors"
|
||||
:data-type="type"
|
||||
>
|
||||
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from './libs/context'
|
||||
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
||||
|
||||
export default {
|
||||
name: 'FormularioInput',
|
||||
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: {
|
||||
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: function (value) {
|
||||
return ['blur', 'live', 'submit'].includes(value)
|
||||
}
|
||||
},
|
||||
showErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
imageBehavior: {
|
||||
type: String,
|
||||
default: 'preview'
|
||||
},
|
||||
uploadUrl: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
uploader: {
|
||||
type: [Function, Object, Boolean],
|
||||
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
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
defaultId: this.$formulario.nextId(this),
|
||||
localAttributes: {},
|
||||
localErrors: [],
|
||||
proxy: this.getInitialValue(),
|
||||
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
||||
formShouldShowErrors: false,
|
||||
validationErrors: [],
|
||||
pendingValidation: Promise.resolve(),
|
||||
// These registries are used for injected messages registrants only (mostly internal).
|
||||
ruleRegistry: [],
|
||||
messageRegistry: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...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: {
|
||||
'$attrs': {
|
||||
handler (value) {
|
||||
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
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.applyInitialValue()
|
||||
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
||||
this.formularioRegister(this.nameOrFallback, this)
|
||||
}
|
||||
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
||||
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||
}
|
||||
this.updateLocalAttributes(this.$attrs)
|
||||
this.performValidation()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
||||
this.removeErrorObserver(this.setErrors)
|
||||
}
|
||||
if (typeof this.formularioDeregister === 'function') {
|
||||
this.formularioDeregister(this.nameOrFallback)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInitialValue () {
|
||||
if (has(this.$options.propsData, 'value')) {
|
||||
return this.value
|
||||
} else if (has(this.$options.propsData, 'formularioValue')) {
|
||||
return this.formularioValue
|
||||
}
|
||||
return ''
|
||||
},
|
||||
applyInitialValue () {
|
||||
// This should only be run immediately on created and ensures that the
|
||||
// proxy and the model are both the same before any additional registration.
|
||||
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
||||
this.context.model = this.proxy
|
||||
}
|
||||
},
|
||||
updateLocalAttributes (value) {
|
||||
if (!shallowEqualObjects(value, this.localAttributes)) {
|
||||
this.localAttributes = value
|
||||
}
|
||||
},
|
||||
performValidation () {
|
||||
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
||||
// Add in ruleRegistry rules. These are added directly via injection from
|
||||
// children and not part of the standard validation rule set.
|
||||
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
||||
this.pendingValidation = this.runRules(rules)
|
||||
.then(messages => this.didValidate(messages))
|
||||
return this.pendingValidation
|
||||
},
|
||||
runRules (rules) {
|
||||
const run = ([rule, args, ruleName, modifier]) => {
|
||||
var res = rule({
|
||||
value: this.context.model,
|
||||
getFormValues: this.getFormValues.bind(this),
|
||||
name: this.context.name
|
||||
}, ...args)
|
||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
||||
return res.then(result => result ? false : this.getMessage(ruleName, args))
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const resolveGroups = (groups, allMessages = []) => {
|
||||
const ruleGroup = groups.shift()
|
||||
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) {
|
||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
||||
this.validationErrors = messages
|
||||
if (validationChanged) {
|
||||
const errorObject = this.getErrorObject()
|
||||
this.$emit('validation', errorObject)
|
||||
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
|
||||
this.formularioFieldValidation(errorObject)
|
||||
}
|
||||
}
|
||||
},
|
||||
getMessage (ruleName, args) {
|
||||
return this.getMessageFunc(ruleName)({
|
||||
args,
|
||||
name: this.mergedValidationName,
|
||||
value: this.context.model,
|
||||
vm: this,
|
||||
formValues: this.getFormValues()
|
||||
})
|
||||
},
|
||||
getMessageFunc (ruleName) {
|
||||
ruleName = snakeToCamel(ruleName)
|
||||
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
||||
switch (typeof this.messages[ruleName]) {
|
||||
case 'function':
|
||||
return this.messages[ruleName]
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
return () => this.messages[ruleName]
|
||||
}
|
||||
}
|
||||
return (context) => this.$formulario.validationMessage(ruleName, context, this)
|
||||
},
|
||||
hasValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => {
|
||||
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
||||
})
|
||||
})
|
||||
},
|
||||
getValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
||||
})
|
||||
},
|
||||
getErrorObject () {
|
||||
return {
|
||||
name: this.context.nameOrFallback || this.context.name,
|
||||
errors: this.validationErrors.filter(s => typeof s === 'string'),
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
removeRule (key) {
|
||||
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
||||
if (ruleIndex >= 0) {
|
||||
this.ruleRegistry.splice(ruleIndex, 1)
|
||||
delete this.messageRegistry[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
352
src/Formulate.js
352
src/Formulate.js
@ -1,352 +0,0 @@
|
||||
import library from './libs/library'
|
||||
import rules from './libs/rules'
|
||||
import mimes from './libs/mimes'
|
||||
import FileUpload from './FileUpload'
|
||||
import { arrayify, parseLocale, has } from './libs/utils'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import { en } from '@braid/vue-formulate-i18n'
|
||||
import fauxUploader from './libs/faux-uploader'
|
||||
import FormulateSlot from './FormulateSlot'
|
||||
import FormulateForm from './FormulateForm.vue'
|
||||
import FormulateInput from './FormulateInput.vue'
|
||||
import FormulateErrors from './FormulateErrors.vue'
|
||||
import FormulateHelp from './slots/FormulateHelp.vue'
|
||||
import FormulateGrouping from './FormulateGrouping.vue'
|
||||
import FormulateLabel from './slots/FormulateLabel.vue'
|
||||
import FormulateAddMore from './slots/FormulateAddMore.vue'
|
||||
import FormulateRepeatable from './slots/FormulateRepeatable.vue'
|
||||
import FormulateInputGroup from './inputs/FormulateInputGroup.vue'
|
||||
import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue'
|
||||
import FormulateRepeatableRemove from './slots/FormulateRepeatableRemove.vue'
|
||||
|
||||
/**
|
||||
* The base formulate library.
|
||||
*/
|
||||
class Formulate {
|
||||
/**
|
||||
* Instantiate our base options.
|
||||
*/
|
||||
constructor () {
|
||||
this.options = {}
|
||||
this.defaults = {
|
||||
components: {
|
||||
FormulateSlot,
|
||||
FormulateForm,
|
||||
FormulateHelp,
|
||||
FormulateLabel,
|
||||
FormulateInput,
|
||||
FormulateErrors,
|
||||
FormulateAddMore,
|
||||
FormulateGrouping,
|
||||
FormulateRepeatable,
|
||||
FormulateInputGroup,
|
||||
FormulateRepeatableRemove,
|
||||
FormulateRepeatableProvider
|
||||
},
|
||||
slotComponents: {
|
||||
label: 'FormulateLabel',
|
||||
help: 'FormulateHelp',
|
||||
errors: 'FormulateErrors',
|
||||
repeatable: 'FormulateRepeatable',
|
||||
addMore: 'FormulateAddMore',
|
||||
remove: 'FormulateRepeatableRemove'
|
||||
},
|
||||
library,
|
||||
rules,
|
||||
mimes,
|
||||
locale: false,
|
||||
uploader: fauxUploader,
|
||||
uploadUrl: false,
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
errorHandler: (err) => err,
|
||||
plugins: [ en ],
|
||||
locales: {},
|
||||
idPrefix: 'formulate-'
|
||||
}
|
||||
this.registry = new Map()
|
||||
this.idRegistry = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install vue formulate, and register it’s components.
|
||||
*/
|
||||
install (Vue, options) {
|
||||
Vue.prototype.$formulate = this
|
||||
this.options = this.defaults
|
||||
var plugins = this.defaults.plugins
|
||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
||||
plugins = plugins.concat(options.plugins)
|
||||
}
|
||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||
this.extend(options || {})
|
||||
for (var componentName in this.options.components) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a deterministically generated id based on the sequence by which it
|
||||
* was requested. This should be *theoretically* the same SSR as client side.
|
||||
* However, SSR and deterministic ids can be very challenging, so this
|
||||
* implementation is open to community review.
|
||||
*/
|
||||
nextId (vm) {
|
||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
||||
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
|
||||
this.idRegistry[pathPrefix] = 0
|
||||
}
|
||||
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of options, apply them to the pre-existing options.
|
||||
* @param {Object} extendWith
|
||||
*/
|
||||
extend (extendWith) {
|
||||
if (typeof extendWith === 'object') {
|
||||
this.options = this.merge(this.options, extendWith)
|
||||
return this
|
||||
}
|
||||
throw new Error(`VueFormulate extend() should be passed an object (was ${typeof extendWith})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object by copying properties of base and mergeWith.
|
||||
* Note: arrays don't overwrite - they push
|
||||
*
|
||||
* @param {Object} base
|
||||
* @param {Object} mergeWith
|
||||
* @param {boolean} concatArrays
|
||||
*/
|
||||
merge (base, mergeWith, concatArrays = true) {
|
||||
var merged = {}
|
||||
for (var key in base) {
|
||||
if (mergeWith.hasOwnProperty(key)) {
|
||||
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
|
||||
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
|
||||
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
|
||||
merged[key] = base[key].concat(mergeWith[key])
|
||||
} else {
|
||||
merged[key] = mergeWith[key]
|
||||
}
|
||||
} else {
|
||||
merged[key] = base[key]
|
||||
}
|
||||
}
|
||||
for (var prop in mergeWith) {
|
||||
if (!merged.hasOwnProperty(prop)) {
|
||||
merged[prop] = mergeWith[prop]
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what "class" of input this element is given the "type".
|
||||
* @param {string} type
|
||||
*/
|
||||
classify (type) {
|
||||
if (this.options.library.hasOwnProperty(type)) {
|
||||
return this.options.library[type].classification
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what type of component to render given the "type".
|
||||
* @param {string} type
|
||||
*/
|
||||
component (type) {
|
||||
if (this.options.library.hasOwnProperty(type)) {
|
||||
return this.options.library[type].component
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* What component should be rendered for the given slot location and type.
|
||||
* @param {string} type the type of component
|
||||
* @param {string} slot the name of the slot
|
||||
*/
|
||||
slotComponent (type, slot) {
|
||||
const def = this.options.library[type]
|
||||
if (def && def.slotComponents && def.slotComponents[slot]) {
|
||||
return def.slotComponents[slot]
|
||||
}
|
||||
return this.options.slotComponents[slot]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules by merging any passed in with global rules.
|
||||
* @return {object} object of validation functions
|
||||
*/
|
||||
rules (rules = {}) {
|
||||
return { ...this.options.rules, ...rules }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get the vue-i18n configured locale.
|
||||
*/
|
||||
i18n (vm) {
|
||||
if (vm.$i18n) {
|
||||
switch (typeof vm.$i18n.locale) {
|
||||
case 'string':
|
||||
return vm.$i18n.locale
|
||||
case 'function':
|
||||
return vm.$i18n.locale()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the proper locale to use.
|
||||
*/
|
||||
getLocale (vm) {
|
||||
if (!this.selectedLocale) {
|
||||
this.selectedLocale = [
|
||||
this.options.locale,
|
||||
this.i18n(vm),
|
||||
'en'
|
||||
].reduce((selection, locale) => {
|
||||
if (selection) {
|
||||
return selection
|
||||
}
|
||||
if (locale) {
|
||||
const option = parseLocale(locale)
|
||||
.find(locale => has(this.options.locales, locale))
|
||||
if (option) {
|
||||
selection = option
|
||||
}
|
||||
}
|
||||
return selection
|
||||
}, false)
|
||||
}
|
||||
return this.selectedLocale
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation message for a particular error.
|
||||
*/
|
||||
validationMessage (rule, validationContext, vm) {
|
||||
const generators = this.options.locales[this.getLocale(vm)]
|
||||
if (generators.hasOwnProperty(rule)) {
|
||||
return generators[rule](validationContext)
|
||||
} else if (rule[0] === '_' && generators.hasOwnProperty(rule.substr(1))) {
|
||||
return generators[rule.substr(1)](validationContext)
|
||||
}
|
||||
if (generators.hasOwnProperty('default')) {
|
||||
return generators.default(validationContext)
|
||||
}
|
||||
return 'This field does not have a valid value'
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a FormulateForm register it.
|
||||
* @param {vm} form
|
||||
*/
|
||||
register (form) {
|
||||
if (form.$options.name === 'FormulateForm' && form.name) {
|
||||
this.registry.set(form.name, form)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a form, remove it from the registry.
|
||||
* @param {vm} form
|
||||
*/
|
||||
deregister (form) {
|
||||
if (
|
||||
form.$options.name === 'FormulateForm' &&
|
||||
form.name &&
|
||||
this.registry.has(form.name)
|
||||
) {
|
||||
this.registry.delete(form.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array, this function will attempt to make sense of the given error
|
||||
* and hydrate a form with the resulting errors.
|
||||
*
|
||||
* @param {error} err
|
||||
* @param {string} formName
|
||||
* @param {error}
|
||||
*/
|
||||
handle (err, formName, skip = false) {
|
||||
const e = skip ? err : this.options.errorHandler(err, formName)
|
||||
if (formName && this.registry.has(formName)) {
|
||||
this.registry.get(formName).applyErrors({
|
||||
formErrors: arrayify(e.formErrors),
|
||||
inputErrors: e.inputErrors || {}
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a form.
|
||||
* @param {string} formName
|
||||
* @param {object} initialValue
|
||||
*/
|
||||
reset (formName, initialValue = {}) {
|
||||
this.resetValidation(formName)
|
||||
this.setValues(formName, initialValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form's validation messages.
|
||||
* @param {string} formName
|
||||
*/
|
||||
resetValidation (formName) {
|
||||
const form = this.registry.get(formName)
|
||||
form.hideErrors(formName)
|
||||
form.namedErrors = []
|
||||
form.namedFieldErrors = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the form values.
|
||||
* @param {string} formName
|
||||
* @param {object} values
|
||||
*/
|
||||
setValues (formName, values) {
|
||||
if (values && !Array.isArray(values) && typeof values === 'object') {
|
||||
const form = this.registry.get(formName)
|
||||
form.setValues({ ...values })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file uploader.
|
||||
*/
|
||||
getUploader () {
|
||||
return this.options.uploader || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global upload url.
|
||||
*/
|
||||
getUploadUrl () {
|
||||
return this.options.uploadUrl || false
|
||||
}
|
||||
|
||||
/**
|
||||
* When re-hydrating a file uploader with an array, get the sub-object key to
|
||||
* access the url of the file. Usually this is just "url".
|
||||
*/
|
||||
getFileUrlKey () {
|
||||
return this.options.fileUrlKey || 'url'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of an upload.
|
||||
*/
|
||||
createUpload (fileList, context) {
|
||||
return new FileUpload(fileList, context, this.options)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Formulate()
|
@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="visibleErrors.length"
|
||||
:class="`formulate-${type}-errors`"
|
||||
>
|
||||
<li
|
||||
v-for="error in visibleErrors"
|
||||
:key="error"
|
||||
:class="`formulate-${type}-error`"
|
||||
v-text="error"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { arrayify } from './libs/utils'
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
observeErrors: {
|
||||
default: false
|
||||
},
|
||||
removeErrorObserver: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'form'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
boundSetErrors: this.setErrors.bind(this),
|
||||
localErrors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleValidationErrors () {
|
||||
return Array.isArray(this.context.visibleValidationErrors) ? this.context.visibleValidationErrors : []
|
||||
},
|
||||
errors () {
|
||||
return Array.isArray(this.context.errors) ? this.context.errors : []
|
||||
},
|
||||
mergedErrors () {
|
||||
return this.errors.concat(this.localErrors)
|
||||
},
|
||||
visibleErrors () {
|
||||
return Array.from(new Set(this.mergedErrors.concat(this.visibleValidationErrors)))
|
||||
.filter(message => typeof message === 'string')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// This registration is for <FormulateErrors /> that are used for displaying
|
||||
// Form errors in an override position.
|
||||
if (this.type === 'form' && typeof this.observeErrors === 'function' && !Array.isArray(this.context.errors)) {
|
||||
this.observeErrors({ callback: this.boundSetErrors, type: this.type })
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (this.type === 'form' && typeof this.removeErrorObserver === 'function' && !Array.isArray(this.context.errors)) {
|
||||
this.removeErrorObserver(this.boundSetErrors)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setErrors (errors) {
|
||||
this.localErrors = arrayify(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="fileUploads.length"
|
||||
class="formulate-files"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileUploads"
|
||||
:key="file.uuid"
|
||||
:data-has-error="!!file.error"
|
||||
:data-has-preview="!!(imagePreview && file.previewData)"
|
||||
>
|
||||
<div class="formulate-file">
|
||||
<div
|
||||
v-if="!!(imagePreview && file.previewData)"
|
||||
class="formulate-file-image-preview"
|
||||
>
|
||||
<img
|
||||
:src="file.previewData"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="formulate-file-name"
|
||||
:title="file.name"
|
||||
v-text="file.name"
|
||||
/>
|
||||
<div
|
||||
v-if="file.progress !== false"
|
||||
:data-just-finished="file.justFinished"
|
||||
:data-is-finished="!file.justFinished && file.complete"
|
||||
class="formulate-file-progress"
|
||||
>
|
||||
<div
|
||||
class="formulate-file-progress-inner"
|
||||
:style="{width: file.progress + '%'}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="(file.complete && !file.justFinished) || file.progress === false"
|
||||
class="formulate-file-remove"
|
||||
@click="file.removeFile"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="file.error"
|
||||
class="formulate-file-upload-error"
|
||||
v-text="file.error"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUpload from './FileUpload'
|
||||
|
||||
export default {
|
||||
name: 'FormulateFiles',
|
||||
props: {
|
||||
files: {
|
||||
type: FileUpload,
|
||||
required: true
|
||||
},
|
||||
imagePreview: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fileUploads () {
|
||||
return this.files.files || []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
files () {
|
||||
if (this.imagePreview) {
|
||||
this.files.loadPreviews()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.imagePreview) {
|
||||
this.files.loadPreviews()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<form
|
||||
:class="classes"
|
||||
@submit.prevent="formSubmitted"
|
||||
>
|
||||
<FormulateErrors
|
||||
v-if="!hasFormErrorObservers"
|
||||
:context="formContext"
|
||||
/>
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { arrayify, has } from './libs/utils'
|
||||
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||
import FormSubmission from './FormSubmission'
|
||||
|
||||
export default {
|
||||
provide () {
|
||||
return {
|
||||
...useRegistryProviders(this),
|
||||
observeErrors: this.addErrorObserver,
|
||||
removeErrorObserver: this.removeErrorObserver,
|
||||
formulateFieldValidation: this.formulateFieldValidation
|
||||
}
|
||||
},
|
||||
name: 'FormulateForm',
|
||||
model: {
|
||||
prop: 'formulateValue',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
formulateValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
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(),
|
||||
formContext () {
|
||||
return {
|
||||
errors: this.mergedFormErrors
|
||||
}
|
||||
},
|
||||
classes () {
|
||||
const classes = { 'formulate-form': true }
|
||||
if (this.name) {
|
||||
classes[`formulate-form--${this.name}`] = true
|
||||
}
|
||||
return classes
|
||||
},
|
||||
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: {
|
||||
formulateValue: {
|
||||
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
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$formulate.register(this)
|
||||
this.applyInitialValues()
|
||||
},
|
||||
destroyed () {
|
||||
this.$formulate.deregister(this)
|
||||
},
|
||||
methods: {
|
||||
...useRegistryMethods(),
|
||||
applyErrors ({ formErrors, inputErrors }) {
|
||||
// given an object of errors, apply them to this form
|
||||
this.namedErrors = formErrors
|
||||
this.namedFieldErrors = inputErrors
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
formulateFieldValidation (errorObject) {
|
||||
this.$emit('validation', errorObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<FormulateSlot
|
||||
name="grouping"
|
||||
class="formulate-input-grouping"
|
||||
:context="context"
|
||||
:force-wrap="context.repeatable"
|
||||
>
|
||||
<FormulateRepeatableProvider
|
||||
v-for="(item, index) in items"
|
||||
:key="item.__id"
|
||||
:index="index"
|
||||
:set-field-value="(field, value) => setFieldValue(index, field, value)"
|
||||
:context="context"
|
||||
@remove="removeItem"
|
||||
>
|
||||
<slot />
|
||||
</FormulateRepeatableProvider>
|
||||
</FormulateSlot>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setId } from './libs/utils'
|
||||
|
||||
export default {
|
||||
name: 'FormulateGrouping',
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
isSubField: () => true,
|
||||
registerProvider: this.registerProvider,
|
||||
deregisterProvider: this.deregisterProvider
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
providers: []
|
||||
}
|
||||
},
|
||||
inject: ['formulateRegisterRule', 'formulateRemoveRule'],
|
||||
computed: {
|
||||
items () {
|
||||
if (Array.isArray(this.context.model)) {
|
||||
if (!this.context.repeatable && this.context.model.length === 0) {
|
||||
return [setId({})]
|
||||
}
|
||||
return this.context.model.map(item => setId(item, item.__id))
|
||||
}
|
||||
return [setId({})]
|
||||
},
|
||||
formShouldShowErrors () {
|
||||
return this.context.formShouldShowErrors
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
providers () {
|
||||
if (this.formShouldShowErrors) {
|
||||
this.showErrors()
|
||||
}
|
||||
},
|
||||
formShouldShowErrors (val) {
|
||||
if (val) {
|
||||
this.showErrors()
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// We register with an error message of 'true' which causes the validation to fail but no message output.
|
||||
this.formulateRegisterRule(this.validateGroup.bind(this), [], 'formulateGrouping', true)
|
||||
},
|
||||
destroyed () {
|
||||
this.formulateRemoveRule('formulateGrouping')
|
||||
},
|
||||
methods: {
|
||||
getAtIndex (index) {
|
||||
if (typeof this.context.model[index] !== 'undefined' && this.context.model[index].__id) {
|
||||
return this.context.model[index]
|
||||
} else if (typeof this.context.model[index] !== 'undefined') {
|
||||
return setId(this.context.model[index])
|
||||
} else if (typeof this.context.model[index] === 'undefined' && typeof this.items[index] !== 'undefined') {
|
||||
return setId({}, this.items[index].__id)
|
||||
}
|
||||
return setId({})
|
||||
},
|
||||
setFieldValue (index, field, value) {
|
||||
const values = Array.isArray(this.context.model) ? this.context.model : []
|
||||
const previous = this.getAtIndex(index)
|
||||
const updated = setId(Object.assign({}, previous, { [field]: value }), previous.__id)
|
||||
values.splice(index, 1, updated)
|
||||
this.context.model = values
|
||||
},
|
||||
validateGroup () {
|
||||
return Promise.all(this.providers.reduce((resolvers, provider) => {
|
||||
if (provider && typeof provider.hasValidationErrors === 'function') {
|
||||
resolvers.push(provider.hasValidationErrors())
|
||||
}
|
||||
return resolvers
|
||||
}, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors))
|
||||
},
|
||||
showErrors () {
|
||||
this.providers.forEach(p => p && typeof p.showErrors === 'function' && p.showErrors())
|
||||
},
|
||||
removeItem (index) {
|
||||
if (Array.isArray(this.context.model)) {
|
||||
this.context.model.splice(index, 1)
|
||||
}
|
||||
},
|
||||
registerProvider (provider) {
|
||||
if (!this.providers.some(p => p === provider)) {
|
||||
this.providers.push(provider)
|
||||
}
|
||||
},
|
||||
deregisterProvider (provider) {
|
||||
this.providers = this.providers.filter(p => p !== provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,396 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input"
|
||||
:data-classification="classification"
|
||||
:data-has-errors="hasErrors"
|
||||
:data-is-showing-errors="hasVisibleErrors"
|
||||
:data-type="type"
|
||||
>
|
||||
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from './libs/context'
|
||||
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInput',
|
||||
inheritAttrs: false,
|
||||
provide () {
|
||||
return {
|
||||
// Allows sub-components of this input to register arbitrary rules.
|
||||
formulateRegisterRule: this.registerRule,
|
||||
formulateRemoveRule: this.removeRule
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
formulateSetter: { default: undefined },
|
||||
formulateFieldValidation: { default: () => () => ({}) },
|
||||
formulateRegister: { default: undefined },
|
||||
formulateDeregister: { default: undefined },
|
||||
getFormValues: { default: () => () => ({}) },
|
||||
observeErrors: { default: undefined },
|
||||
removeErrorObserver: { default: undefined },
|
||||
isSubField: { default: () => () => false }
|
||||
},
|
||||
model: {
|
||||
prop: 'formulateValue',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
name: {
|
||||
type: [String, Boolean],
|
||||
default: true
|
||||
},
|
||||
/* eslint-disable */
|
||||
formulateValue: {
|
||||
default: ''
|
||||
},
|
||||
value: {
|
||||
default: false
|
||||
},
|
||||
/* eslint-enable */
|
||||
options: {
|
||||
type: [Object, Array, Boolean],
|
||||
default: false
|
||||
},
|
||||
optionGroups: {
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: [String, Boolean, Number],
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
labelPosition: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: Infinity
|
||||
},
|
||||
help: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
helpPosition: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
errors: {
|
||||
type: [String, Array, Boolean],
|
||||
default: false
|
||||
},
|
||||
repeatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
validation: {
|
||||
type: [String, Boolean, Array],
|
||||
default: false
|
||||
},
|
||||
validationName: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
errorBehavior: {
|
||||
type: String,
|
||||
default: 'blur',
|
||||
validator: function (value) {
|
||||
return ['blur', 'live', 'submit'].includes(value)
|
||||
}
|
||||
},
|
||||
showErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
imageBehavior: {
|
||||
type: String,
|
||||
default: 'preview'
|
||||
},
|
||||
uploadUrl: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
uploader: {
|
||||
type: [Function, Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
uploadBehavior: {
|
||||
type: String,
|
||||
default: 'live'
|
||||
},
|
||||
preventWindowDrops: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showValue: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
validationMessages: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
validationRules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
checked: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
disableErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
addLabel: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
defaultId: this.$formulate.nextId(this),
|
||||
localAttributes: {},
|
||||
localErrors: [],
|
||||
proxy: this.getInitialValue(),
|
||||
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
||||
formShouldShowErrors: false,
|
||||
validationErrors: [],
|
||||
pendingValidation: Promise.resolve(),
|
||||
// These registries are used for injected messages registrants only (mostly internal).
|
||||
ruleRegistry: [],
|
||||
messageRegistry: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...context,
|
||||
classification () {
|
||||
const classification = this.$formulate.classify(this.type)
|
||||
return (classification === 'box' && this.options) ? 'group' : classification
|
||||
},
|
||||
component () {
|
||||
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
|
||||
},
|
||||
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: {
|
||||
'$attrs': {
|
||||
handler (value) {
|
||||
this.updateLocalAttributes(value)
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
proxy (newValue, oldValue) {
|
||||
this.performValidation()
|
||||
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
},
|
||||
formulateValue (newValue, oldValue) {
|
||||
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
},
|
||||
showValidationErrors: {
|
||||
handler (val) {
|
||||
this.$emit('error-visibility', val)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.applyInitialValue()
|
||||
if (this.formulateRegister && typeof this.formulateRegister === 'function') {
|
||||
this.formulateRegister(this.nameOrFallback, this)
|
||||
}
|
||||
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
||||
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||
}
|
||||
this.updateLocalAttributes(this.$attrs)
|
||||
this.performValidation()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
||||
this.removeErrorObserver(this.setErrors)
|
||||
}
|
||||
if (typeof this.formulateDeregister === 'function') {
|
||||
this.formulateDeregister(this.nameOrFallback)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInitialValue () {
|
||||
// Manually request classification, pre-computed props
|
||||
var classification = this.$formulate.classify(this.type)
|
||||
classification = (classification === 'box' && this.options) ? 'group' : classification
|
||||
if (classification === 'box' && this.checked) {
|
||||
return this.value || true
|
||||
} else if (has(this.$options.propsData, 'value') && classification !== 'box') {
|
||||
return this.value
|
||||
} else if (has(this.$options.propsData, 'formulateValue')) {
|
||||
return this.formulateValue
|
||||
}
|
||||
return ''
|
||||
},
|
||||
applyInitialValue () {
|
||||
// This should only be run immediately on created and ensures that the
|
||||
// proxy and the model are both the same before any additional registration.
|
||||
if (
|
||||
!shallowEqualObjects(this.context.model, this.proxy) &&
|
||||
// we dont' want to set the model if we are a sub-box of a multi-box field
|
||||
(Object.prototype.hasOwnProperty(this.$options.propsData, 'options') && this.classification === 'box')
|
||||
) {
|
||||
this.context.model = this.proxy
|
||||
}
|
||||
},
|
||||
updateLocalAttributes (value) {
|
||||
if (!shallowEqualObjects(value, this.localAttributes)) {
|
||||
this.localAttributes = value
|
||||
}
|
||||
},
|
||||
performValidation () {
|
||||
let rules = parseRules(this.validation, this.$formulate.rules(this.parsedValidationRules))
|
||||
// Add in ruleRegistry rules. These are added directly via injection from
|
||||
// children and not part of the standard validation rule set.
|
||||
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
||||
this.pendingValidation = this.runRules(rules)
|
||||
.then(messages => this.didValidate(messages))
|
||||
return this.pendingValidation
|
||||
},
|
||||
runRules (rules) {
|
||||
const run = ([rule, args, ruleName, modifier]) => {
|
||||
var res = rule({
|
||||
value: this.context.model,
|
||||
getFormValues: this.getFormValues.bind(this),
|
||||
name: this.context.name
|
||||
}, ...args)
|
||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
||||
return res.then(result => result ? false : this.getMessage(ruleName, args))
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const resolveGroups = (groups, allMessages = []) => {
|
||||
const ruleGroup = groups.shift()
|
||||
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) {
|
||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
||||
this.validationErrors = messages
|
||||
if (validationChanged) {
|
||||
const errorObject = this.getErrorObject()
|
||||
this.$emit('validation', errorObject)
|
||||
if (this.formulateFieldValidation && typeof this.formulateFieldValidation === 'function') {
|
||||
this.formulateFieldValidation(errorObject)
|
||||
}
|
||||
}
|
||||
},
|
||||
getMessage (ruleName, args) {
|
||||
return this.getMessageFunc(ruleName)({
|
||||
args,
|
||||
name: this.mergedValidationName,
|
||||
value: this.context.model,
|
||||
vm: this,
|
||||
formValues: this.getFormValues()
|
||||
})
|
||||
},
|
||||
getMessageFunc (ruleName) {
|
||||
ruleName = snakeToCamel(ruleName)
|
||||
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
||||
switch (typeof this.messages[ruleName]) {
|
||||
case 'function':
|
||||
return this.messages[ruleName]
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
return () => this.messages[ruleName]
|
||||
}
|
||||
}
|
||||
return (context) => this.$formulate.validationMessage(ruleName, context, this)
|
||||
},
|
||||
hasValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => {
|
||||
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
||||
})
|
||||
})
|
||||
},
|
||||
getValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
||||
})
|
||||
},
|
||||
getErrorObject () {
|
||||
return {
|
||||
name: this.context.nameOrFallback || this.context.name,
|
||||
errors: this.validationErrors.filter(s => typeof s === 'string'),
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
removeRule (key) {
|
||||
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
||||
if (ruleIndex >= 0) {
|
||||
this.ruleRegistry.splice(ruleIndex, 1)
|
||||
delete this.messageRegistry[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Default base for input components.
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
type () {
|
||||
return this.context.type
|
||||
},
|
||||
id () {
|
||||
return this.context.id
|
||||
},
|
||||
attributes () {
|
||||
return this.context.attributes || {}
|
||||
},
|
||||
hasValue () {
|
||||
return !!this.context.model
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<FormulateSlot
|
||||
name="repeatable"
|
||||
:context="context"
|
||||
:index="index"
|
||||
:remove-item="removeItem"
|
||||
>
|
||||
<component
|
||||
:is="context.slotComponents.repeatable"
|
||||
:context="context"
|
||||
:index="index"
|
||||
:remove-item="removeItem"
|
||||
>
|
||||
<FormulateSlot
|
||||
:context="context"
|
||||
:index="index"
|
||||
name="default"
|
||||
/>
|
||||
</component>
|
||||
</FormulateSlot>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||
|
||||
export default {
|
||||
provide () {
|
||||
return {
|
||||
...useRegistryProviders(this),
|
||||
formulateSetter: (field, value) => this.setFieldValue(field, value)
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
registerProvider: 'registerProvider',
|
||||
deregisterProvider: 'deregisterProvider'
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
setFieldValue: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
...useRegistry(this),
|
||||
isGrouping: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...useRegistryComputed()
|
||||
},
|
||||
created () {
|
||||
this.registerProvider(this)
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.deregisterProvider(this)
|
||||
},
|
||||
methods: {
|
||||
...useRegistryMethods(['setFieldValue']),
|
||||
removeItem () {
|
||||
this.$emit('remove', this.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,36 +0,0 @@
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
functional: true,
|
||||
render (h, { props, data, parent, children }) {
|
||||
var p = parent
|
||||
var { name, forceWrap, context, ...mergeWithContext } = props
|
||||
|
||||
// Look up the ancestor tree for the first FormulateInput
|
||||
while (p && p.$options.name !== 'FormulateInput') {
|
||||
p = p.$parent
|
||||
}
|
||||
|
||||
// if we never found the proper parent, just end it.
|
||||
if (!p) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If we found a formulate input, check for a matching scoped slot
|
||||
if (p.$scopedSlots && p.$scopedSlots[props.name]) {
|
||||
return p.$scopedSlots[props.name]({ ...context, ...mergeWithContext })
|
||||
}
|
||||
|
||||
// If we found no scoped slot, take the children and render those inside a wrapper if there are multiple
|
||||
if (Array.isArray(children) && (children.length > 1 || (forceWrap && children.length > 0))) {
|
||||
const { name, context, ...attrs } = data.attrs
|
||||
return h('div', { ...data, ...{ attrs } }, children)
|
||||
|
||||
// If there is only one child, render it alone
|
||||
} else if (Array.isArray(children) && children.length === 1) {
|
||||
return children[0]
|
||||
}
|
||||
|
||||
// If there are no children, render nothing
|
||||
return null
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input-group"
|
||||
:data-is-repeatable="context.repeatable"
|
||||
>
|
||||
<template
|
||||
v-if="subType !== 'grouping'"
|
||||
>
|
||||
<FormulateInput
|
||||
v-for="optionContext in optionsWithContext"
|
||||
:key="optionContext.id"
|
||||
v-model="context.model"
|
||||
v-bind="optionContext"
|
||||
:disable-errors="true"
|
||||
class="formulate-input-group-item"
|
||||
@blur="context.blurHandler"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<FormulateGrouping
|
||||
:context="context"
|
||||
>
|
||||
<slot />
|
||||
</FormulateGrouping>
|
||||
<FormulateSlot
|
||||
v-if="canAddMore"
|
||||
name="addmore"
|
||||
:context="context"
|
||||
:add-more="addItem"
|
||||
>
|
||||
<component
|
||||
:is="context.slotComponents.addMore"
|
||||
:context="context"
|
||||
:add-more="addItem"
|
||||
@add="addItem"
|
||||
/>
|
||||
</FormulateSlot>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setId } from '../libs/utils'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInputGroup',
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options () {
|
||||
return this.context.options || []
|
||||
},
|
||||
subType () {
|
||||
return (this.context.type === 'group') ? 'grouping' : 'inputs'
|
||||
},
|
||||
optionsWithContext () {
|
||||
const {
|
||||
// The following are a list of items to pull out of the context object
|
||||
attributes: { id, ...groupApplicableAttributes },
|
||||
blurHandler,
|
||||
classification,
|
||||
component,
|
||||
getValidationErrors,
|
||||
hasLabel,
|
||||
hasValidationErrors,
|
||||
isSubField,
|
||||
labelPosition,
|
||||
options,
|
||||
performValidation,
|
||||
setErrors,
|
||||
slotComponents,
|
||||
validationErrors,
|
||||
visibleValidationErrors,
|
||||
help,
|
||||
...context
|
||||
} = this.context
|
||||
return this.options.map(option => this.groupItemContext(
|
||||
context,
|
||||
option,
|
||||
groupApplicableAttributes
|
||||
))
|
||||
},
|
||||
canAddMore () {
|
||||
return (this.context.repeatable && this.items.length < this.context.limit)
|
||||
},
|
||||
items () {
|
||||
return Array.isArray(this.context.model) ? this.context.model : [{}]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addItem () {
|
||||
if (Array.isArray(this.context.model)) {
|
||||
this.context.model.push(setId({}))
|
||||
return
|
||||
}
|
||||
this.context.model = this.items.concat([setId({})])
|
||||
},
|
||||
groupItemContext (context, option, groupAttributes) {
|
||||
const optionAttributes = {}
|
||||
const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes, !context.hasGivenName ? {
|
||||
name: true
|
||||
} : {})
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -6,101 +6,50 @@ import { map, arrayify, shallowEqualObjects } from './utils'
|
||||
* @return {object}
|
||||
*/
|
||||
export default {
|
||||
context () {
|
||||
return defineModel.call(this, {
|
||||
addLabel: this.logicalAddLabel,
|
||||
attributes: this.elementAttributes,
|
||||
blurHandler: blurHandler.bind(this),
|
||||
classification: this.classification,
|
||||
component: this.component,
|
||||
disableErrors: this.disableErrors,
|
||||
errors: this.explicitErrors,
|
||||
allErrors: this.allErrors,
|
||||
formShouldShowErrors: this.formShouldShowErrors,
|
||||
getValidationErrors: this.getValidationErrors.bind(this),
|
||||
hasGivenName: this.hasGivenName,
|
||||
hasLabel: (this.label && this.classification !== 'button'),
|
||||
hasValidationErrors: this.hasValidationErrors.bind(this),
|
||||
help: this.help,
|
||||
helpPosition: this.logicalHelpPosition,
|
||||
id: this.id || this.defaultId,
|
||||
imageBehavior: this.imageBehavior,
|
||||
label: this.label,
|
||||
labelPosition: this.logicalLabelPosition,
|
||||
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,
|
||||
slotComponents: this.slotComponents,
|
||||
type: this.type,
|
||||
uploadBehavior: this.uploadBehavior,
|
||||
uploadUrl: this.mergedUploadUrl,
|
||||
uploader: this.uploader || this.$formulate.getUploader(),
|
||||
validationErrors: this.validationErrors,
|
||||
value: this.value,
|
||||
visibleValidationErrors: this.visibleValidationErrors,
|
||||
isSubField: this.isSubField,
|
||||
...this.typeContext
|
||||
})
|
||||
},
|
||||
// Used in sub-context
|
||||
nameOrFallback,
|
||||
hasGivenName,
|
||||
typeContext,
|
||||
elementAttributes,
|
||||
logicalLabelPosition,
|
||||
logicalHelpPosition,
|
||||
mergedUploadUrl,
|
||||
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,
|
||||
slotComponents,
|
||||
logicalAddLabel
|
||||
}
|
||||
|
||||
/**
|
||||
* The label to display when adding a new group.
|
||||
*/
|
||||
function logicalAddLabel () {
|
||||
if (typeof this.addLabel === 'boolean') {
|
||||
return `+ ${this.label || this.name || 'Add'}`
|
||||
}
|
||||
return this.addLabel
|
||||
}
|
||||
|
||||
/**
|
||||
* Given (this.type), return an object to merge with the context
|
||||
* @return {object}
|
||||
* @return {object}
|
||||
*/
|
||||
function typeContext () {
|
||||
switch (this.classification) {
|
||||
case 'select':
|
||||
return {
|
||||
options: createOptionList.call(this, this.options),
|
||||
optionGroups: this.optionGroups ? map(this.optionGroups, (k, v) => createOptionList.call(this, v)) : false,
|
||||
placeholder: this.$attrs.placeholder || false
|
||||
}
|
||||
case 'slider':
|
||||
return { showValue: !!this.showValue }
|
||||
default:
|
||||
if (this.options) {
|
||||
return {
|
||||
options: createOptionList.call(this, this.options)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
// These items are not passed as context
|
||||
isVmodeled,
|
||||
mergedValidationName,
|
||||
explicitErrors,
|
||||
allErrors,
|
||||
hasErrors,
|
||||
hasVisibleErrors,
|
||||
showValidationErrors,
|
||||
visibleValidationErrors
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,71 +57,35 @@ function typeContext () {
|
||||
* @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
|
||||
}
|
||||
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`
|
||||
}
|
||||
// If there is help text, have this element be described by it.
|
||||
if (this.help) {
|
||||
attrs['aria-describedby'] = `${attrs.id}-help`
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the best-guess location for the label (before or after).
|
||||
* @return {string} before|after
|
||||
*/
|
||||
function logicalLabelPosition () {
|
||||
if (this.labelPosition) {
|
||||
return this.labelPosition
|
||||
}
|
||||
switch (this.classification) {
|
||||
case 'box':
|
||||
return 'after'
|
||||
default:
|
||||
return 'before'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the best location for the label based on type (before or after).
|
||||
*/
|
||||
function logicalHelpPosition () {
|
||||
if (this.helpPosition) {
|
||||
return this.helpPosition
|
||||
}
|
||||
switch (this.classification) {
|
||||
case 'group':
|
||||
return 'before'
|
||||
default:
|
||||
return 'after'
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* The validation label to use.
|
||||
*/
|
||||
function mergedValidationName () {
|
||||
if (this.validationName) {
|
||||
return this.validationName
|
||||
}
|
||||
if (typeof this.name === 'string') {
|
||||
if (this.validationName) {
|
||||
return this.validationName
|
||||
}
|
||||
|
||||
return this.name
|
||||
}
|
||||
if (this.label) {
|
||||
return this.label
|
||||
}
|
||||
return this.type
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,7 +93,7 @@ function mergedValidationName () {
|
||||
* that is defined as a plugin option.
|
||||
*/
|
||||
function mergedUploadUrl () {
|
||||
return this.uploadUrl || this.$formulate.getUploadUrl()
|
||||
return this.uploadUrl || this.$formulario.getUploadUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -188,13 +101,11 @@ function mergedUploadUrl () {
|
||||
* @return {boolean}
|
||||
*/
|
||||
function showValidationErrors () {
|
||||
if (this.showErrors || this.formShouldShowErrors) {
|
||||
return true
|
||||
}
|
||||
if (this.classification === 'file' && this.uploadBehavior === 'live' && modelGetter.call(this)) {
|
||||
return true
|
||||
}
|
||||
return this.behavioralErrorVisibility
|
||||
if (this.showErrors || this.formShouldShowErrors) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.behavioralErrorVisibility
|
||||
}
|
||||
|
||||
/**
|
||||
@ -202,37 +113,35 @@ function showValidationErrors () {
|
||||
* @return {array}
|
||||
*/
|
||||
function visibleValidationErrors () {
|
||||
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
|
||||
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the element’s name, or select a fallback.
|
||||
*/
|
||||
function nameOrFallback () {
|
||||
if (this.name === true && this.classification !== 'button') {
|
||||
return `${this.type}_${this.elementAttributes.id}`
|
||||
}
|
||||
if (this.name === false || (this.classification === 'button' && this.name === true)) {
|
||||
return false
|
||||
}
|
||||
return this.name
|
||||
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'
|
||||
return typeof this.name !== 'boolean'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this formulate element is v-modeled or not.
|
||||
* Determines if this formulario element is v-modeled or not.
|
||||
*/
|
||||
function isVmodeled () {
|
||||
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,70 +151,56 @@ function isVmodeled () {
|
||||
* @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}` })
|
||||
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 optionList
|
||||
}
|
||||
return options
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* These are errors we that have been explicity passed to us.
|
||||
*/
|
||||
function explicitErrors () {
|
||||
return arrayify(this.errors)
|
||||
.concat(this.localErrors)
|
||||
.concat(arrayify(this.error))
|
||||
return arrayify(this.errors)
|
||||
.concat(this.localErrors)
|
||||
.concat(arrayify(this.error))
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged errors computed property.
|
||||
*/
|
||||
function allErrors () {
|
||||
return this.explicitErrors
|
||||
.concat(arrayify(this.validationErrors))
|
||||
return this.explicitErrors
|
||||
.concat(arrayify(this.validationErrors))
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this computed property have errors
|
||||
*/
|
||||
function hasErrors () {
|
||||
return !!this.allErrors.length
|
||||
return !!this.allErrors.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if form has actively visible errors (of any kind)
|
||||
*/
|
||||
function hasVisibleErrors () {
|
||||
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* The component that should be rendered in the label slot as default.
|
||||
*/
|
||||
function slotComponents () {
|
||||
return {
|
||||
label: this.$formulate.slotComponent(this.type, 'label'),
|
||||
help: this.$formulate.slotComponent(this.type, 'help'),
|
||||
errors: this.$formulate.slotComponent(this.type, 'errors'),
|
||||
repeatable: this.$formulate.slotComponent(this.type, 'repeatable'),
|
||||
addMore: this.$formulate.slotComponent(this.type, 'addMore'),
|
||||
remove: this.$formulate.slotComponent(this.type, 'remove')
|
||||
}
|
||||
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
|
||||
}
|
||||
this.$emit('blur')
|
||||
if (this.errorBehavior === 'blur') {
|
||||
this.behavioralErrorVisibility = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -313,35 +208,32 @@ function blurHandler () {
|
||||
* @param {object} context
|
||||
*/
|
||||
function defineModel (context) {
|
||||
return Object.defineProperty(context, 'model', {
|
||||
get: modelGetter.bind(this),
|
||||
set: modelSetter.bind(this)
|
||||
})
|
||||
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 ? 'formulateValue' : 'proxy'
|
||||
if (this.type === 'checkbox' && !Array.isArray(this[model]) && this.options) {
|
||||
return []
|
||||
}
|
||||
if (!this[model]) {
|
||||
return ''
|
||||
}
|
||||
return this[model]
|
||||
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.formulateSetter === 'function') {
|
||||
this.formulateSetter(this.context.name, 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)
|
||||
}
|
||||
}
|
||||
|
@ -7,27 +7,27 @@
|
||||
* @param {object} options
|
||||
*/
|
||||
export default function (file, progress, error, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
|
||||
const start = performance.now()
|
||||
return new Promise((resolve, reject) => {
|
||||
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
|
||||
const start = performance.now()
|
||||
|
||||
/**
|
||||
* Create a recursive timeout that advances the progress.
|
||||
*/
|
||||
const advance = () => setTimeout(() => {
|
||||
const elapsed = performance.now() - start
|
||||
const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100))
|
||||
progress(currentProgress)
|
||||
/**
|
||||
* Create a recursive timeout that advances the progress.
|
||||
*/
|
||||
const advance = () => setTimeout(() => {
|
||||
const elapsed = performance.now() - start
|
||||
const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100))
|
||||
progress(currentProgress)
|
||||
|
||||
if (currentProgress >= 100) {
|
||||
return resolve({
|
||||
url: 'http://via.placeholder.com/350x150.png',
|
||||
name: file.name
|
||||
})
|
||||
} else {
|
||||
if (currentProgress >= 100) {
|
||||
return resolve({
|
||||
url: 'http://via.placeholder.com/350x150.png',
|
||||
name: file.name
|
||||
})
|
||||
} else {
|
||||
advance()
|
||||
}
|
||||
}, 20)
|
||||
advance()
|
||||
}
|
||||
}, 20)
|
||||
advance()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -4,8 +4,8 @@
|
||||
* on the documentation site vueformulate.com.
|
||||
*/
|
||||
export default function (err) {
|
||||
if (typeof err === 'object' && err.response) {
|
||||
if (typeof err === 'object' && err.response) {
|
||||
|
||||
}
|
||||
return {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
@ -4,51 +4,51 @@
|
||||
* Note: We're shipping front end code here, file size is critical. This file is
|
||||
* overly terse for that reason alone, we wouldn't necessarily recommend this.
|
||||
*/
|
||||
const fi = 'FormulateInput'
|
||||
const fi = 'FormularioInput'
|
||||
const add = (n, c) => ({
|
||||
classification: n,
|
||||
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))
|
||||
classification: n,
|
||||
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))
|
||||
})
|
||||
export default {
|
||||
// === SINGLE LINE TEXT STYLE INPUTS
|
||||
...[
|
||||
'text',
|
||||
'email',
|
||||
'number',
|
||||
'color',
|
||||
'date',
|
||||
'hidden',
|
||||
'month',
|
||||
'password',
|
||||
'search',
|
||||
'tel',
|
||||
'time',
|
||||
'url',
|
||||
'week',
|
||||
'datetime-local'
|
||||
].reduce((lib, type) => ({ ...lib, [type]: add('text') }), {}),
|
||||
// === SINGLE LINE TEXT STYLE INPUTS
|
||||
...[
|
||||
'text',
|
||||
'email',
|
||||
'number',
|
||||
'color',
|
||||
'date',
|
||||
'hidden',
|
||||
'month',
|
||||
'password',
|
||||
'search',
|
||||
'tel',
|
||||
'time',
|
||||
'url',
|
||||
'week',
|
||||
'datetime-local'
|
||||
].reduce((lib, type) => ({ ...lib, [type]: add('text') }), {}),
|
||||
|
||||
// === SLIDER INPUTS
|
||||
range: add('slider'),
|
||||
// === SLIDER INPUTS
|
||||
range: add('slider'),
|
||||
|
||||
// === MULTI LINE TEXT INPUTS
|
||||
textarea: add('textarea', 'TextArea'),
|
||||
// === MULTI LINE TEXT INPUTS
|
||||
textarea: add('textarea', 'TextArea'),
|
||||
|
||||
// === BOX STYLE INPUTS
|
||||
checkbox: add('box'),
|
||||
radio: add('box'),
|
||||
// === BOX STYLE INPUTS
|
||||
checkbox: add('box'),
|
||||
radio: add('box'),
|
||||
|
||||
// === BUTTON STYLE INPUTS
|
||||
submit: add('button'),
|
||||
button: add('button'),
|
||||
// === BUTTON STYLE INPUTS
|
||||
submit: add('button'),
|
||||
button: add('button'),
|
||||
|
||||
// === SELECT STYLE INPUTS
|
||||
select: add('select'),
|
||||
// === SELECT STYLE INPUTS
|
||||
select: add('select'),
|
||||
|
||||
// === FILE TYPE
|
||||
file: add('file'),
|
||||
image: add('file'),
|
||||
// === FILE TYPE
|
||||
file: add('file'),
|
||||
image: add('file'),
|
||||
|
||||
// === GROUP TYPE
|
||||
group: add('group')
|
||||
// === GROUP TYPE
|
||||
group: add('group')
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
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'
|
||||
'csv': 'text/csv',
|
||||
'gif': i + 'gif',
|
||||
'jpg': i + 'jpeg',
|
||||
'jpeg': i + 'jpeg',
|
||||
'png': i + 'png',
|
||||
'pdf': 'application/pdf',
|
||||
'svg': i + 'svg+xml'
|
||||
}
|
||||
|
@ -1,129 +1,129 @@
|
||||
import { shallowEqualObjects, has } from './utils'
|
||||
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} key
|
||||
* @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
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
this.registry.set(field, component)
|
||||
const hasVModelValue = has(component.$options.propsData, 'formulateValue')
|
||||
const hasValue = has(component.$options.propsData, 'value')
|
||||
if (
|
||||
!hasVModelValue &&
|
||||
this.ctx.hasInitialValue &&
|
||||
this.ctx.initialValues[field]
|
||||
) {
|
||||
// In the case that the form is carrying an initial value and the
|
||||
// element is not, set it directly.
|
||||
component.context.model = this.ctx.initialValues[field]
|
||||
} else if (
|
||||
(hasVModelValue || hasValue) &&
|
||||
!shallowEqualObjects(component.proxy, 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
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* Add an item to the registry.
|
||||
* @param {string|array} key
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,119 +131,119 @@ class Registry {
|
||||
* @param {component} contextComponent
|
||||
*/
|
||||
export default function useRegistry (contextComponent) {
|
||||
const registry = new Registry(contextComponent)
|
||||
return registry.dataProps()
|
||||
const registry = new Registry(contextComponent)
|
||||
return registry.dataProps()
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed properties related to the registry.
|
||||
*/
|
||||
export function useRegistryComputed () {
|
||||
return {
|
||||
hasInitialValue () {
|
||||
return (
|
||||
(this.formulateValue && typeof this.formulateValue === 'object') ||
|
||||
(this.values && typeof this.values === 'object') ||
|
||||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
||||
)
|
||||
},
|
||||
isVmodeled () {
|
||||
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
},
|
||||
initialValues () {
|
||||
if (
|
||||
has(this.$options.propsData, 'formulateValue') &&
|
||||
typeof this.formulateValue === 'object'
|
||||
) {
|
||||
// If there is a v-model on the form/group, use those values as first priority
|
||||
return Object.assign({}, this.formulateValue) // @todo - use a deep clone to detach reference types
|
||||
} else if (
|
||||
has(this.$options.propsData, 'values') &&
|
||||
typeof this.values === 'object'
|
||||
) {
|
||||
// If there are values, use them as secondary priority
|
||||
return Object.assign({}, this.values)
|
||||
} else if (
|
||||
this.isGrouping && typeof this.context.model[this.index] === 'object'
|
||||
) {
|
||||
return this.context.model[this.index]
|
||||
}
|
||||
return {}
|
||||
return {
|
||||
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 !!(this.$options.propsData.hasOwnProperty('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 {
|
||||
Object.assign(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(values[field], this.proxy[field]) &&
|
||||
!shallowEqualObjects(values[field], this.registry.get(field).proxy)
|
||||
) {
|
||||
this.setFieldValue(field, values[field])
|
||||
this.registry.get(field).context.model = values[field]
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return Object.keys(methods).reduce((withMethods, key) => {
|
||||
return without.includes(key) ? withMethods : { ...withMethods, [key]: methods[key] }
|
||||
}, {})
|
||||
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 {
|
||||
formulateSetter: ctx.setFieldValue,
|
||||
formulateRegister: ctx.register,
|
||||
formulateDeregister: ctx.deregister,
|
||||
getFormValues: ctx.getFormValues
|
||||
}
|
||||
return {
|
||||
formularioSetter: ctx.setFieldValue,
|
||||
formularioRegister: ctx.register,
|
||||
formularioDeregister: ctx.deregister,
|
||||
getFormValues: ctx.getFormValues
|
||||
}
|
||||
}
|
||||
|
@ -6,287 +6,287 @@ import { shallowEqualObjects, regexForFormat } from './utils'
|
||||
* Library of rules
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Rule: the value must be "yes", "on", "1", or true
|
||||
*/
|
||||
accepted: function ({ value }) {
|
||||
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
|
||||
},
|
||||
/**
|
||||
* Rule: the value must be "yes", "on", "1", or true
|
||||
*/
|
||||
accepted: function ({ 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
|
||||
*/
|
||||
after: function ({ value }, compare = false) {
|
||||
const timestamp = Date.parse(compare || new Date())
|
||||
const fieldValue = Date.parse(value)
|
||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
||||
},
|
||||
/**
|
||||
* Rule: checks if a value is after a given date. Defaults to current time
|
||||
*/
|
||||
after: function ({ value }, compare = false) {
|
||||
const timestamp = Date.parse(compare || new Date())
|
||||
const fieldValue = Date.parse(value)
|
||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is only alpha
|
||||
*/
|
||||
alpha: function ({ value }, set = 'default') {
|
||||
const sets = {
|
||||
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
||||
latin: /^[a-zA-Z]+$/
|
||||
}
|
||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||
return Promise.resolve(sets[selectedSet].test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is alpha numeric
|
||||
*/
|
||||
alphanumeric: function ({ value }, set = 'default') {
|
||||
const sets = {
|
||||
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
|
||||
latin: /^[a-zA-Z0-9]+$/
|
||||
}
|
||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||
return Promise.resolve(sets[selectedSet].test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a value is after a given date. Defaults to current time
|
||||
*/
|
||||
before: function ({ value }, compare = false) {
|
||||
const timestamp = Date.parse(compare || new Date())
|
||||
const fieldValue = Date.parse(value)
|
||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is between two other values
|
||||
*/
|
||||
between: function ({ value }, from = 0, to = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (from === null || to === null || isNaN(from) || isNaN(to)) {
|
||||
return false
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = Number(value)
|
||||
from = Number(from)
|
||||
to = Number(to)
|
||||
return (value > from && value < to)
|
||||
}
|
||||
if (typeof value === 'string' || force === 'length') {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length > from && value.length < to
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm that the value of one field is the same as another, mostly used
|
||||
* for password confirmations.
|
||||
*/
|
||||
confirm: function ({ value, getFormValues, name }, field) {
|
||||
return Promise.resolve((() => {
|
||||
const formValues = getFormValues()
|
||||
var confirmationFieldName = field
|
||||
if (!confirmationFieldName) {
|
||||
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
||||
}
|
||||
return formValues[confirmationFieldName] === value
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: ensures the value is a date according to Date.parse(), or a format
|
||||
* regex.
|
||||
*/
|
||||
date: function ({ value }, format = false) {
|
||||
return Promise.resolve((() => {
|
||||
if (format && typeof format === 'string') {
|
||||
return regexForFormat(format).test(value)
|
||||
}
|
||||
return !isNaN(Date.parse(value))
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: tests
|
||||
*/
|
||||
email: function ({ value }) {
|
||||
// eslint-disable-next-line
|
||||
const isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
return Promise.resolve(isEmail.test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value ends with one of the given Strings
|
||||
*/
|
||||
endsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.endsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value is in an array (stack).
|
||||
*/
|
||||
in: function ({ value }, ...stack) {
|
||||
return Promise.resolve(stack.find(item => {
|
||||
if (typeof item === 'object') {
|
||||
return shallowEqualObjects(item, value)
|
||||
}
|
||||
return item === value
|
||||
}) !== undefined)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Match the value against a (stack) of patterns or strings
|
||||
*/
|
||||
matches: function ({ value }, ...stack) {
|
||||
return Promise.resolve(!!stack.find(pattern => {
|
||||
if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') {
|
||||
pattern = new RegExp(pattern.substr(1, pattern.length - 2))
|
||||
}
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern.test(value)
|
||||
}
|
||||
return pattern === value
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the file type is correct.
|
||||
*/
|
||||
mime: function ({ value }, ...types) {
|
||||
return Promise.resolve((() => {
|
||||
if (value instanceof FileUpload) {
|
||||
const fileList = value.getFiles()
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i].file
|
||||
if (!types.includes(file.type)) {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* Rule: checks if the value is only alpha
|
||||
*/
|
||||
alpha: function ({ value }, set = 'default') {
|
||||
const sets = {
|
||||
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
||||
latin: /^[a-zA-Z]+$/
|
||||
}
|
||||
}
|
||||
return true
|
||||
})())
|
||||
},
|
||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||
return Promise.resolve(sets[selectedSet].test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the minimum value of a particular.
|
||||
*/
|
||||
min: function ({ value }, minimum = 1, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (Array.isArray(value)) {
|
||||
minimum = !isNaN(minimum) ? Number(minimum) : minimum
|
||||
return value.length >= minimum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = !isNaN(value) ? Number(value) : value
|
||||
return value >= minimum
|
||||
}
|
||||
if (typeof value === 'string' || (force === 'length')) {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length >= minimum
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
/**
|
||||
* Rule: checks if the value is alpha numeric
|
||||
*/
|
||||
alphanumeric: function ({ value }, set = 'default') {
|
||||
const sets = {
|
||||
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
|
||||
latin: /^[a-zA-Z0-9]+$/
|
||||
}
|
||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||
return Promise.resolve(sets[selectedSet].test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the maximum value of a particular.
|
||||
*/
|
||||
max: function ({ value }, maximum = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (Array.isArray(value)) {
|
||||
maximum = !isNaN(maximum) ? Number(maximum) : maximum
|
||||
return value.length <= maximum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = !isNaN(value) ? Number(value) : value
|
||||
return value <= maximum
|
||||
}
|
||||
if (typeof value === 'string' || (force === 'length')) {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length <= maximum
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
/**
|
||||
* Rule: checks if a value is after a given date. Defaults to current time
|
||||
*/
|
||||
before: function ({ value }, compare = false) {
|
||||
const timestamp = Date.parse(compare || new Date())
|
||||
const fieldValue = Date.parse(value)
|
||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value is not in stack.
|
||||
*/
|
||||
not: function ({ value }, ...stack) {
|
||||
return Promise.resolve(stack.find(item => {
|
||||
if (typeof item === 'object') {
|
||||
return shallowEqualObjects(item, value)
|
||||
}
|
||||
return item === value
|
||||
}) === undefined)
|
||||
},
|
||||
/**
|
||||
* Rule: checks if the value is between two other values
|
||||
*/
|
||||
between: function ({ value }, from = 0, to = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (from === null || to === null || isNaN(from) || isNaN(to)) {
|
||||
return false
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = Number(value)
|
||||
from = Number(from)
|
||||
to = Number(to)
|
||||
return (value > from && value < to)
|
||||
}
|
||||
if (typeof value === 'string' || force === 'length') {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length > from && value.length < to
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is only alpha numeric
|
||||
*/
|
||||
number: function ({ value }) {
|
||||
return Promise.resolve(!isNaN(value))
|
||||
},
|
||||
/**
|
||||
* Confirm that the value of one field is the same as another, mostly used
|
||||
* for password confirmations.
|
||||
*/
|
||||
confirm: function ({ value, getFormValues, name }, field) {
|
||||
return Promise.resolve((() => {
|
||||
const formValues = getFormValues()
|
||||
var confirmationFieldName = field
|
||||
if (!confirmationFieldName) {
|
||||
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
||||
}
|
||||
return formValues[confirmationFieldName] === value
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: must be a value
|
||||
*/
|
||||
required: function ({ value }, isRequired = true) {
|
||||
return Promise.resolve((() => {
|
||||
if (!isRequired || ['no', 'false'].includes(isRequired)) {
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return !!value.length
|
||||
}
|
||||
if (value instanceof FileUpload) {
|
||||
return value.getFiles().length > 0
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return !!value
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return (!value) ? false : !!Object.keys(value).length
|
||||
}
|
||||
return true
|
||||
})())
|
||||
},
|
||||
/**
|
||||
* Rule: ensures the value is a date according to Date.parse(), or a format
|
||||
* regex.
|
||||
*/
|
||||
date: function ({ value }, format = false) {
|
||||
return Promise.resolve((() => {
|
||||
if (format && typeof format === 'string') {
|
||||
return regexForFormat(format).test(value)
|
||||
}
|
||||
return !isNaN(Date.parse(value))
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value starts with one of the given Strings
|
||||
*/
|
||||
startsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.startsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
/**
|
||||
* Rule: tests
|
||||
*/
|
||||
email: function ({ value }) {
|
||||
// eslint-disable-next-line
|
||||
const isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
return Promise.resolve(isEmail.test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a string is a valid url
|
||||
*/
|
||||
url: function ({ value }) {
|
||||
return Promise.resolve(isUrl(value))
|
||||
},
|
||||
/**
|
||||
* Rule: Value ends with one of the given Strings
|
||||
*/
|
||||
endsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.endsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: not a true rule — more like a compiler flag.
|
||||
*/
|
||||
bail: function () {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
/**
|
||||
* Rule: Value is in an array (stack).
|
||||
*/
|
||||
in: function ({ value }, ...stack) {
|
||||
return Promise.resolve(stack.find(item => {
|
||||
if (typeof item === 'object') {
|
||||
return shallowEqualObjects(item, value)
|
||||
}
|
||||
return item === value
|
||||
}) !== undefined)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Match the value against a (stack) of patterns or strings
|
||||
*/
|
||||
matches: function ({ value }, ...stack) {
|
||||
return Promise.resolve(!!stack.find(pattern => {
|
||||
if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') {
|
||||
pattern = new RegExp(pattern.substr(1, pattern.length - 2))
|
||||
}
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern.test(value)
|
||||
}
|
||||
return pattern === value
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the file type is correct.
|
||||
*/
|
||||
mime: function ({ value }, ...types) {
|
||||
return Promise.resolve((() => {
|
||||
if (value instanceof FileUpload) {
|
||||
const fileList = value.getFiles()
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i].file
|
||||
if (!types.includes(file.type)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the minimum value of a particular.
|
||||
*/
|
||||
min: function ({ value }, minimum = 1, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (Array.isArray(value)) {
|
||||
minimum = !isNaN(minimum) ? Number(minimum) : minimum
|
||||
return value.length >= minimum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = !isNaN(value) ? Number(value) : value
|
||||
return value >= minimum
|
||||
}
|
||||
if (typeof value === 'string' || (force === 'length')) {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length >= minimum
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the maximum value of a particular.
|
||||
*/
|
||||
max: function ({ value }, maximum = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (Array.isArray(value)) {
|
||||
maximum = !isNaN(maximum) ? Number(maximum) : maximum
|
||||
return value.length <= maximum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = !isNaN(value) ? Number(value) : value
|
||||
return value <= maximum
|
||||
}
|
||||
if (typeof value === 'string' || (force === 'length')) {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length <= maximum
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value is not in stack.
|
||||
*/
|
||||
not: function ({ value }, ...stack) {
|
||||
return Promise.resolve(stack.find(item => {
|
||||
if (typeof item === 'object') {
|
||||
return shallowEqualObjects(item, value)
|
||||
}
|
||||
return item === value
|
||||
}) === undefined)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is only alpha numeric
|
||||
*/
|
||||
number: function ({ value }) {
|
||||
return Promise.resolve(!isNaN(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: must be a value
|
||||
*/
|
||||
required: function ({ value }, isRequired = true) {
|
||||
return Promise.resolve((() => {
|
||||
if (!isRequired || ['no', 'false'].includes(isRequired)) {
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return !!value.length
|
||||
}
|
||||
if (value instanceof FileUpload) {
|
||||
return value.getFiles().length > 0
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return !!value
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return (!value) ? false : !!Object.keys(value).length
|
||||
}
|
||||
return true
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value starts with one of the given Strings
|
||||
*/
|
||||
startsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.startsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a string is a valid url
|
||||
*/
|
||||
url: function ({ value }) {
|
||||
return Promise.resolve(isUrl(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: not a true rule — more like a compiler flag.
|
||||
*/
|
||||
bail: function () {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import FileUpload from '../FileUpload'
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function map (original, callback) {
|
||||
const obj = {}
|
||||
for (let key in original) {
|
||||
obj[key] = callback(key, original[key])
|
||||
}
|
||||
return obj
|
||||
const obj = {}
|
||||
for (let key in original) {
|
||||
obj[key] = callback(key, original[key])
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
@ -19,28 +19,28 @@ export function map (original, callback) {
|
||||
* @param {*} objB
|
||||
*/
|
||||
export function shallowEqualObjects (objA, objB) {
|
||||
if (objA === objB) {
|
||||
return true
|
||||
}
|
||||
if (!objA || !objB) {
|
||||
return false
|
||||
}
|
||||
var aKeys = Object.keys(objA)
|
||||
var bKeys = Object.keys(objB)
|
||||
var len = aKeys.length
|
||||
|
||||
if (bKeys.length !== len) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var key = aKeys[i]
|
||||
|
||||
if (objA[key] !== objB[key]) {
|
||||
return false
|
||||
if (objA === objB) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
if (!objA || !objB) {
|
||||
return false
|
||||
}
|
||||
var aKeys = Object.keys(objA)
|
||||
var bKeys = Object.keys(objB)
|
||||
var len = aKeys.length
|
||||
|
||||
if (bKeys.length !== len) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var key = aKeys[i]
|
||||
|
||||
if (objA[key] !== objB[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,15 +48,15 @@ export function shallowEqualObjects (objA, objB) {
|
||||
* @param {String} string
|
||||
*/
|
||||
export function snakeToCamel (string) {
|
||||
if (typeof string === 'string') {
|
||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
return $1.toUpperCase().replace('_', '')
|
||||
}
|
||||
return $1
|
||||
})
|
||||
}
|
||||
return string
|
||||
if (typeof string === 'string') {
|
||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
return $1.toUpperCase().replace('_', '')
|
||||
}
|
||||
return $1
|
||||
})
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,19 +64,19 @@ export function snakeToCamel (string) {
|
||||
* @param {mixed} item
|
||||
*/
|
||||
export function arrayify (item) {
|
||||
if (!item) {
|
||||
if (!item) {
|
||||
return []
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return [item]
|
||||
}
|
||||
if (Array.isArray(item)) {
|
||||
return item
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.values(item)
|
||||
}
|
||||
return []
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return [item]
|
||||
}
|
||||
if (Array.isArray(item)) {
|
||||
return item
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.values(item)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,13 +86,13 @@ export function arrayify (item) {
|
||||
* @return {array} an array of functions
|
||||
*/
|
||||
export function parseRules (validation, rules) {
|
||||
if (typeof validation === 'string') {
|
||||
return parseRules(validation.split('|'), rules)
|
||||
}
|
||||
if (!Array.isArray(validation)) {
|
||||
return []
|
||||
}
|
||||
return validation.map(rule => parseRule(rule, rules)).filter(f => !!f)
|
||||
if (typeof validation === 'string') {
|
||||
return parseRules(validation.split('|'), rules)
|
||||
}
|
||||
if (!Array.isArray(validation)) {
|
||||
return []
|
||||
}
|
||||
return validation.map(rule => parseRule(rule, rules)).filter(f => !!f)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,29 +101,29 @@ export function parseRules (validation, rules) {
|
||||
* @param {string|function} rule
|
||||
*/
|
||||
function parseRule (rule, rules) {
|
||||
if (typeof rule === 'function') {
|
||||
return [rule, []]
|
||||
}
|
||||
if (Array.isArray(rule) && rule.length) {
|
||||
rule = rule.map(r => r) // light clone
|
||||
const [ruleName, modifier] = parseModifier(rule.shift())
|
||||
if (typeof ruleName === 'string' && rules.hasOwnProperty(ruleName)) {
|
||||
return [rules[ruleName], rule, ruleName, modifier]
|
||||
if (typeof rule === 'function') {
|
||||
return [rule, []]
|
||||
}
|
||||
if (typeof ruleName === 'function') {
|
||||
return [ruleName, rule, ruleName, modifier]
|
||||
if (Array.isArray(rule) && rule.length) {
|
||||
rule = rule.map(r => r) // light clone
|
||||
const [ruleName, modifier] = parseModifier(rule.shift())
|
||||
if (typeof ruleName === 'string' && rules.hasOwnProperty(ruleName)) {
|
||||
return [rules[ruleName], rule, ruleName, modifier]
|
||||
}
|
||||
if (typeof ruleName === 'function') {
|
||||
return [ruleName, rule, ruleName, modifier]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof rule === 'string') {
|
||||
const segments = rule.split(':')
|
||||
const [ruleName, modifier] = parseModifier(segments.shift())
|
||||
if (rules.hasOwnProperty(ruleName)) {
|
||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
||||
} else {
|
||||
throw new Error(`Unknown validation rule ${rule}`)
|
||||
if (typeof rule === 'string') {
|
||||
const segments = rule.split(':')
|
||||
const [ruleName, modifier] = parseModifier(segments.shift())
|
||||
if (rules.hasOwnProperty(ruleName)) {
|
||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
||||
} else {
|
||||
throw new Error(`Unknown validation rule ${rule}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,10 +132,10 @@ function parseRule (rule, rules) {
|
||||
* @return {array} [ruleName, modifier]
|
||||
*/
|
||||
function parseModifier (ruleName) {
|
||||
if (/^[\^]/.test(ruleName.charAt(0))) {
|
||||
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
||||
}
|
||||
return [snakeToCamel(ruleName), null]
|
||||
if (/^[\^]/.test(ruleName.charAt(0))) {
|
||||
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
||||
}
|
||||
return [snakeToCamel(ruleName), null]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,40 +152,40 @@ function parseModifier (ruleName) {
|
||||
* @param {array} rules
|
||||
*/
|
||||
export function groupBails (rules) {
|
||||
const groups = []
|
||||
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||
if (bailIndex >= 0) {
|
||||
// Get all the rules until the first bail rule (dont include the bail)
|
||||
const preBail = rules.splice(0, bailIndex + 1).slice(0, -1)
|
||||
// Rules before the `bail` rule are non-bailing
|
||||
preBail.length && groups.push(preBail)
|
||||
// All remaining rules are bailing rule groups
|
||||
rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true })))
|
||||
} else {
|
||||
groups.push(rules)
|
||||
}
|
||||
|
||||
return groups.reduce((groups, group) => {
|
||||
const splitByMod = (group, bailGroup = false) => {
|
||||
if (group.length < 2) {
|
||||
return Object.defineProperty([group], 'bail', { value: bailGroup })
|
||||
}
|
||||
const splits = []
|
||||
const modIndex = group.findIndex(([,,, modifier]) => modifier === '^')
|
||||
if (modIndex >= 0) {
|
||||
const preMod = group.splice(0, modIndex)
|
||||
// rules before the modifier are non-bailing rules.
|
||||
preMod.length && splits.push(...splitByMod(preMod, bailGroup))
|
||||
splits.push(Object.defineProperty([group.shift()], 'bail', { value: true }))
|
||||
// rules after the modifier are non-bailing rules.
|
||||
group.length && splits.push(...splitByMod(group, bailGroup))
|
||||
} else {
|
||||
splits.push(group)
|
||||
}
|
||||
return splits
|
||||
const groups = []
|
||||
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||
if (bailIndex >= 0) {
|
||||
// Get all the rules until the first bail rule (dont include the bail)
|
||||
const preBail = rules.splice(0, bailIndex + 1).slice(0, -1)
|
||||
// Rules before the `bail` rule are non-bailing
|
||||
preBail.length && groups.push(preBail)
|
||||
// All remaining rules are bailing rule groups
|
||||
rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true })))
|
||||
} else {
|
||||
groups.push(rules)
|
||||
}
|
||||
return groups.concat(splitByMod(group))
|
||||
}, [])
|
||||
|
||||
return groups.reduce((groups, group) => {
|
||||
const splitByMod = (group, bailGroup = false) => {
|
||||
if (group.length < 2) {
|
||||
return Object.defineProperty([group], 'bail', { value: bailGroup })
|
||||
}
|
||||
const splits = []
|
||||
const modIndex = group.findIndex(([,,, modifier]) => modifier === '^')
|
||||
if (modIndex >= 0) {
|
||||
const preMod = group.splice(0, modIndex)
|
||||
// rules before the modifier are non-bailing rules.
|
||||
preMod.length && splits.push(...splitByMod(preMod, bailGroup))
|
||||
splits.push(Object.defineProperty([group.shift()], 'bail', { value: true }))
|
||||
// rules after the modifier are non-bailing rules.
|
||||
group.length && splits.push(...splitByMod(group, bailGroup))
|
||||
} else {
|
||||
splits.push(group)
|
||||
}
|
||||
return splits
|
||||
}
|
||||
return groups.concat(splitByMod(group))
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,7 +193,7 @@ export function groupBails (rules) {
|
||||
* @param {string} string
|
||||
*/
|
||||
export function escapeRegExp (string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -201,18 +201,18 @@ export function escapeRegExp (string) {
|
||||
* @param {string} format
|
||||
*/
|
||||
export function regexForFormat (format) {
|
||||
let escaped = `^${escapeRegExp(format)}$`
|
||||
const formats = {
|
||||
MM: '(0[1-9]|1[012])',
|
||||
M: '([1-9]|1[012])',
|
||||
DD: '([012][1-9]|3[01])',
|
||||
D: '([012]?[1-9]|3[01])',
|
||||
YYYY: '\\d{4}',
|
||||
YY: '\\d{2}'
|
||||
}
|
||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||
return regex.replace(format, formats[format])
|
||||
}, escaped))
|
||||
let escaped = `^${escapeRegExp(format)}$`
|
||||
const formats = {
|
||||
MM: '(0[1-9]|1[012])',
|
||||
M: '([1-9]|1[012])',
|
||||
DD: '([012][1-9]|3[01])',
|
||||
D: '([012]?[1-9]|3[01])',
|
||||
YYYY: '\\d{4}',
|
||||
YY: '\\d{2}'
|
||||
}
|
||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||
return regex.replace(format, formats[format])
|
||||
}, escaped))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,19 +220,19 @@ export function regexForFormat (format) {
|
||||
* @param {mixed} data
|
||||
*/
|
||||
export function isValueType (data) {
|
||||
switch (typeof data) {
|
||||
case 'symbol':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
if (data === null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
switch (typeof data) {
|
||||
case 'symbol':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
if (data === null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,19 +240,19 @@ export function isValueType (data) {
|
||||
* case of needing to unbind reactive watchers.
|
||||
*/
|
||||
export function cloneDeep (obj) {
|
||||
if (typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
const isArr = Array.isArray(obj)
|
||||
const newObj = isArr ? [] : {}
|
||||
for (const key in obj) {
|
||||
if (obj[key] instanceof FileUpload || isValueType(obj[key])) {
|
||||
newObj[key] = obj[key]
|
||||
} else {
|
||||
newObj[key] = cloneDeep(obj[key])
|
||||
if (typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
const isArr = Array.isArray(obj)
|
||||
const newObj = isArr ? [] : {}
|
||||
for (const key in obj) {
|
||||
if (obj[key] instanceof FileUpload || isValueType(obj[key])) {
|
||||
newObj[key] = obj[key]
|
||||
} else {
|
||||
newObj[key] = cloneDeep(obj[key])
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,20 +260,20 @@ export function cloneDeep (obj) {
|
||||
* @param {string} locale
|
||||
*/
|
||||
export function parseLocale (locale) {
|
||||
const segments = locale.split('-')
|
||||
return segments.reduce((options, segment) => {
|
||||
if (options.length) {
|
||||
options.unshift(`${options[0]}-${segment}`)
|
||||
}
|
||||
return options.length ? options : [segment]
|
||||
}, [])
|
||||
const segments = locale.split('-')
|
||||
return segments.reduce((options, segment) => {
|
||||
if (options.length) {
|
||||
options.unshift(`${options[0]}-${segment}`)
|
||||
}
|
||||
return options.length ? options : [segment]
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||
*/
|
||||
export function has (ctx, prop) {
|
||||
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -282,5 +282,65 @@ export function has (ctx, prop) {
|
||||
* @param {Symbol} id
|
||||
*/
|
||||
export function setId (o, id) {
|
||||
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) {
|
||||
let fieldParts = field.split('.');
|
||||
|
||||
let result = obj;
|
||||
for (const key in fieldParts) {
|
||||
let matches = fieldParts[key].match(/(.+)\[(\d+)\]$/);
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
if (matches) {
|
||||
result = result[matches[1]];
|
||||
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
result = result[matches[2]];
|
||||
} else {
|
||||
result = result[fieldParts[key]];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function setNested(obj, field, value) {
|
||||
let fieldParts = field.split('.');
|
||||
|
||||
let subProxy = obj;
|
||||
for (let i = 0; i < fieldParts.length; i++) {
|
||||
let fieldPart = fieldParts[i];
|
||||
|
||||
let matches = fieldPart.match(/(.+)\[(\d+)\]$/);
|
||||
|
||||
if (matches) {
|
||||
if (subProxy[matches[1]] === undefined) {
|
||||
subProxy[matches[1]] = [];
|
||||
}
|
||||
subProxy = subProxy[matches[1]];
|
||||
|
||||
if (i == fieldParts.length - 1) {
|
||||
subProxy[matches[2]] = value
|
||||
break;
|
||||
} else {
|
||||
subProxy = subProxy[matches[2]];
|
||||
}
|
||||
} else {
|
||||
if (i == fieldParts.length - 1) {
|
||||
subProxy[fieldPart] = value
|
||||
break;
|
||||
} else {
|
||||
if (subProxy[fieldPart] === undefined) {
|
||||
subProxy[fieldPart] = {};
|
||||
}
|
||||
subProxy = subProxy[fieldPart];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
# i18n moved
|
||||
|
||||
Locales have been removed from vue-formulate core to [vue-formulate-i18n](https://github.com/wearebraid/vue-formulate-i18n).
|
||||
This was done to allow for better tree-shaking by bundlers and allow
|
||||
for lots of additional language support without increasing the size of the core package.
|
||||
|
||||
## PRs welcome
|
||||
|
||||
[Please read the i18n contribution documentation](https://www.vueformulate.com/guide/contributing/#internationalization).
|
@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="formulate-input-group-add-more">
|
||||
<FormulateInput
|
||||
type="button"
|
||||
:label="context.addLabel"
|
||||
data-minor
|
||||
data-ghost
|
||||
@click="addMore"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
addMore: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="context.help"
|
||||
:id="`${context.id}-help`"
|
||||
:class="`formulate-input-help formulate-input-help--${context.helpPosition}`"
|
||||
v-text="context.help"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<label
|
||||
:class="`formulate-input-label formulate-input-label--${context.labelPosition}`"
|
||||
:for="context.id"
|
||||
v-text="context.label"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input-group-repeatable"
|
||||
>
|
||||
<FormulateSlot
|
||||
name="remove"
|
||||
:context="context"
|
||||
:remove-item="removeItem"
|
||||
>
|
||||
<component
|
||||
:is="context.slotComponents.remove"
|
||||
:context="context"
|
||||
:remove-item="removeItem"
|
||||
/>
|
||||
</FormulateSlot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
removeItem: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<a
|
||||
v-if="context.repeatable"
|
||||
class="formulate-input-group-repeatable-remove"
|
||||
role="button"
|
||||
@click.prevent="removeItem"
|
||||
@keypress.enter="removeItem"
|
||||
v-text="`Remove`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
removeItem: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user