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**
|
**Describe the new feature you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
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%
|
80%
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 RetailDriver LLC
|
||||||
Copyright (c) 2020 Braid LLC
|
Copyright (c) 2020 Braid LLC
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
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'
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/Formulate.js', // Path relative to package.json
|
input: 'src/Formulario.js', // Path relative to package.json
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: 'Formulate',
|
name: 'Formulario',
|
||||||
exports: 'default',
|
exports: 'default',
|
||||||
globals: {
|
globals: {
|
||||||
'is-plain-object': 'isPlainObject',
|
'is-plain-object': 'isPlainObject',
|
||||||
'nanoid/non-secure': 'nanoid',
|
'nanoid/non-secure': 'nanoid',
|
||||||
'is-url': 'isUrl',
|
'is-url': 'isUrl',
|
||||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
|
||||||
},
|
},
|
||||||
sourcemap: false
|
sourcemap: false
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,15 @@ import internal from 'rollup-plugin-internal'
|
|||||||
import { terser } from 'rollup-plugin-terser'
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/Formulate.js', // Path relative to package.json
|
input: 'src/Formulario.js', // Path relative to package.json
|
||||||
output: {
|
output: {
|
||||||
name: 'VueFormulate',
|
name: 'VueFormulario',
|
||||||
exports: 'default',
|
exports: 'default',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
globals: {
|
globals: {
|
||||||
'is-plain-object': 'isPlainObject',
|
'is-plain-object': 'isPlainObject',
|
||||||
'nanoid/non-secure': 'nanoid',
|
'nanoid/non-secure': 'nanoid',
|
||||||
'is-url': 'isUrl',
|
'is-url': 'isUrl',
|
||||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -24,7 +23,7 @@ export default {
|
|||||||
preferBuiltins: false
|
preferBuiltins: false
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url', '@braid/vue-formulate-i18n']),
|
internal(['is-plain-object', 'nanoid/non-secure', 'is-url']),
|
||||||
vue({
|
vue({
|
||||||
css: true, // Dynamically inject css as a <style> tag
|
css: true, // Dynamically inject css as a <style> tag
|
||||||
compileTemplate: true // Explicitly convert template to render function
|
compileTemplate: true // Explicitly convert template to render function
|
||||||
|
173
package.json
173
package.json
@ -1,89 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "@braid/vue-formulate",
|
"name": "@retailcrm/vue-formulario",
|
||||||
"version": "2.3.0",
|
"version": "0.1.0",
|
||||||
"description": "The easiest way to build forms in Vue.",
|
"main": "dist/formulario.umd.js",
|
||||||
"main": "dist/formulate.umd.js",
|
"module": "dist/formulario.esm.js",
|
||||||
"module": "dist/formulate.esm.js",
|
"unpkg": "dist/formulario.min.js",
|
||||||
"unpkg": "dist/formulate.min.js",
|
"publishConfig": {
|
||||||
"publishConfig": {
|
"access": "public"
|
||||||
"access": "public"
|
},
|
||||||
},
|
"browser": {
|
||||||
"browser": {
|
"./sfc": "src/Formulario.js"
|
||||||
"./sfc": "src/Formulate.js"
|
},
|
||||||
},
|
"scripts": {
|
||||||
"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": "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/formulario.esm.js",
|
||||||
"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/formulario.umd.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/formulario.min.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": "NODE_ENV=test jest --config test/jest.conf.js",
|
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
|
||||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
|
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
||||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
"build:size": "gzip -c dist/formulario.esm.js | wc -c",
|
||||||
"build:size": "gzip -c dist/formulate.esm.js | wc -c",
|
"dev": "vue-cli-service serve --port=7872 examples/main.js"
|
||||||
"dev": "vue-cli-service serve --port=7872 examples/main.js"
|
},
|
||||||
},
|
"repository": {
|
||||||
"repository": {
|
"type": "git",
|
||||||
"type": "git",
|
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
|
||||||
"url": "git+ssh://git@github.com/wearebraid/vue-formulate.git"
|
},
|
||||||
},
|
"keywords": [
|
||||||
"keywords": [
|
"vue",
|
||||||
"vue",
|
"form",
|
||||||
"form",
|
"forms",
|
||||||
"forms",
|
"validation",
|
||||||
"validation",
|
"validate"
|
||||||
"vuex",
|
],
|
||||||
"validate"
|
"author": "RetailDriverLLC <integration@retailcrm.ru>",
|
||||||
],
|
"contributors": [
|
||||||
"author": "Justin Schroeder <justin@wearebraid.com>",
|
"Justin Schroeder <justin@wearebraid.com>"
|
||||||
"license": "MIT",
|
],
|
||||||
"bugs": {
|
"license": "MIT",
|
||||||
"url": "https://github.com/wearebraid/vue-formulate/issues"
|
"bugs": {
|
||||||
},
|
"url": "https://github.com/retailcrm/vue-formulario/issues"
|
||||||
"homepage": "https://www.vueformulate.com",
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.6",
|
"@babel/core": "^7.9.6",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
|
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
|
||||||
"@babel/preset-env": "^7.9.6",
|
"@babel/preset-env": "^7.9.6",
|
||||||
"@rollup/plugin-buble": "^0.21.3",
|
"@rollup/plugin-buble": "^0.21.3",
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||||
"@vue/cli-plugin-babel": "^4.3.1",
|
"@vue/cli-plugin-babel": "^4.3.1",
|
||||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||||
"@vue/cli-service": "^4.3.1",
|
"@vue/cli-service": "^4.3.1",
|
||||||
"@vue/component-compiler-utils": "^3.1.2",
|
"@vue/component-compiler-utils": "^3.1.2",
|
||||||
"@vue/test-utils": "^1.0.2",
|
"@vue/test-utils": "^1.0.2",
|
||||||
"autoprefixer": "^9.7.6",
|
"autoprefixer": "^9.7.6",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^25.5.1",
|
"babel-jest": "^25.5.1",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"eslint-config-standard": "^12.0.0",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-import": "^2.20.1",
|
||||||
"eslint-plugin-node": "^8.0.1",
|
"eslint-plugin-node": "^8.0.1",
|
||||||
"eslint-plugin-promise": "^4.1.1",
|
"eslint-plugin-promise": "^4.1.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^5.2.3",
|
"eslint-plugin-vue": "^5.2.3",
|
||||||
"flush-promises": "^1.0.2",
|
"flush-promises": "^1.0.2",
|
||||||
"jest": "^25.5.4",
|
"jest": "^25.5.4",
|
||||||
"jest-vue-preprocessor": "^1.7.1",
|
"jest-vue-preprocessor": "^1.7.1",
|
||||||
"rollup": "^1.32.1",
|
"rollup": "^1.32.1",
|
||||||
"rollup-plugin-auto-external": "^2.0.0",
|
"rollup-plugin-auto-external": "^2.0.0",
|
||||||
"rollup-plugin-internal": "^1.0.4",
|
"rollup-plugin-internal": "^1.0.4",
|
||||||
"rollup-plugin-multi-input": "^1.1.1",
|
"rollup-plugin-multi-input": "^1.1.1",
|
||||||
"rollup-plugin-terser": "^5.3.0",
|
"rollup-plugin-terser": "^5.3.0",
|
||||||
"rollup-plugin-vue": "^5.1.7",
|
"rollup-plugin-vue": "^5.1.7",
|
||||||
"typescript": "^3.9.2",
|
"typescript": "^3.9.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-jest": "^3.0.5",
|
"vue-jest": "^3.0.5",
|
||||||
"vue-runtime-helpers": "^1.1.2",
|
"vue-runtime-helpers": "^1.1.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
"vue-template-es2015-compiler": "^1.9.1",
|
"vue-template-es2015-compiler": "^1.9.1",
|
||||||
"watch": "^1.0.2"
|
"watch": "^1.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate-i18n": "^1.6.2",
|
"is-plain-object": "^3.0.0",
|
||||||
"is-plain-object": "^3.0.0",
|
"is-url": "^1.2.4",
|
||||||
"is-url": "^1.2.4",
|
"nanoid": "^2.1.11"
|
||||||
"nanoid": "^2.1.11"
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,210 +5,210 @@ import nanoid from 'nanoid/non-secure'
|
|||||||
* the upload flow.
|
* the upload flow.
|
||||||
*/
|
*/
|
||||||
class FileUpload {
|
class FileUpload {
|
||||||
/**
|
/**
|
||||||
* Create a file upload object.
|
* Create a file upload object.
|
||||||
* @param {FileList} fileList
|
* @param {FileList} fileList
|
||||||
* @param {object} context
|
* @param {object} context
|
||||||
*/
|
*/
|
||||||
constructor (input, context, options = {}) {
|
constructor (input, context, options = {}) {
|
||||||
this.input = input
|
this.input = input
|
||||||
this.fileList = input.files
|
this.fileList = input.files
|
||||||
this.files = []
|
this.files = []
|
||||||
this.options = { ...{ mimes: {} }, ...options }
|
this.options = { ...{ mimes: {} }, ...options }
|
||||||
this.results = false
|
this.results = false
|
||||||
this.context = context
|
this.context = context
|
||||||
if (Array.isArray(this.fileList)) {
|
if (Array.isArray(this.fileList)) {
|
||||||
this.rehydrateFileList(this.fileList)
|
this.rehydrateFileList(this.fileList)
|
||||||
} else {
|
} else {
|
||||||
this.addFileList(this.fileList)
|
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))
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.then(res => res.data)
|
|
||||||
.catch(err => args[2](err))
|
|
||||||
}
|
}
|
||||||
return this.context.uploader(...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the file upload.
|
* Given a pre-existing array of files, create a faux FileList.
|
||||||
*/
|
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
|
||||||
upload () {
|
* @param {string} pathKey the object-key to access the url (defaults to "url")
|
||||||
if (this.results) {
|
*/
|
||||||
return Promise.resolve(this.results)
|
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'))
|
* Produce an array of files and alert the callback.
|
||||||
}
|
* @param {FileList}
|
||||||
Promise.all(this.files.map(file => {
|
*/
|
||||||
return file.path ? Promise.resolve(file.path) : this.getUploader(
|
addFileList (fileList) {
|
||||||
file.file,
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
(progress) => {
|
const file = fileList[i]
|
||||||
file.progress = progress
|
const uuid = nanoid()
|
||||||
if (progress >= 100) {
|
const removeFile = function () {
|
||||||
if (!file.complete) {
|
this.removeFile(uuid)
|
||||||
file.justFinished = true
|
|
||||||
setTimeout(() => { file.justFinished = false }, this.options.uploadJustCompleteDuration)
|
|
||||||
}
|
|
||||||
file.complete = true
|
|
||||||
}
|
}
|
||||||
},
|
this.files.push({
|
||||||
(error) => {
|
progress: false,
|
||||||
file.progress = 0
|
error: false,
|
||||||
file.error = error
|
complete: false,
|
||||||
file.complete = true
|
justFinished: false,
|
||||||
},
|
name: file.name || 'file-upload',
|
||||||
this.options
|
file,
|
||||||
)
|
uuid,
|
||||||
}))
|
path: false,
|
||||||
.then(results => {
|
removeFile: removeFile.bind(this),
|
||||||
this.results = results
|
previewData: file.previewData || false
|
||||||
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.
|
* Check if the file has an.
|
||||||
*/
|
*/
|
||||||
loadPreviews () {
|
hasUploader () {
|
||||||
this.files.map(file => {
|
return !!this.context.uploader
|
||||||
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.
|
* 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:
|
||||||
getFileList () {
|
*
|
||||||
return this.fileList
|
* 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.
|
* Get a new uploader function.
|
||||||
*/
|
*/
|
||||||
getFiles () {
|
getUploader (...args) {
|
||||||
return this.files
|
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'
|
* Perform the file upload.
|
||||||
return this.results ? JSON.stringify(this.results, null, ' ') : `FileUpload(${descriptor})`
|
*/
|
||||||
}
|
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
|
export default FileUpload
|
||||||
|
@ -2,41 +2,41 @@ import { cloneDeep } from './libs/utils'
|
|||||||
import FileUpload from './FileUpload'
|
import FileUpload from './FileUpload'
|
||||||
|
|
||||||
export default class FormSubmission {
|
export default class FormSubmission {
|
||||||
/**
|
/**
|
||||||
* Initialize a formulate form.
|
* Initialize a formulario form.
|
||||||
* @param {vm} form an instance of FormulateForm
|
* @param {vm} form an instance of FormularioForm
|
||||||
*/
|
*/
|
||||||
constructor (form) {
|
constructor (form) {
|
||||||
this.form = form
|
this.form = form
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the form has any validation errors.
|
* Determine if the form has any validation errors.
|
||||||
*
|
*
|
||||||
* @return {Promise} resolves a boolean
|
* @return {Promise} resolves a boolean
|
||||||
*/
|
*/
|
||||||
hasValidationErrors () {
|
hasValidationErrors () {
|
||||||
return this.form.hasValidationErrors()
|
return this.form.hasValidationErrors()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously generate the values payload of this form.
|
* Asynchronously generate the values payload of this form.
|
||||||
* @return {Promise} resolves to json
|
* @return {Promise} resolves to json
|
||||||
*/
|
*/
|
||||||
values () {
|
values () {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const pending = []
|
const pending = []
|
||||||
const values = cloneDeep(this.form.proxy)
|
const values = cloneDeep(this.form.proxy)
|
||||||
for (const key in values) {
|
for (const key in values) {
|
||||||
if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) {
|
if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) {
|
||||||
pending.push(
|
pending.push(
|
||||||
this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Promise.all(pending)
|
Promise.all(pending)
|
||||||
.then(() => resolve(values))
|
.then(() => resolve(values))
|
||||||
.catch(err => reject(err))
|
.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}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
context () {
|
context () {
|
||||||
return defineModel.call(this, {
|
return defineModel.call(this, {
|
||||||
addLabel: this.logicalAddLabel,
|
attributes: this.elementAttributes,
|
||||||
attributes: this.elementAttributes,
|
blurHandler: blurHandler.bind(this),
|
||||||
blurHandler: blurHandler.bind(this),
|
disableErrors: this.disableErrors,
|
||||||
classification: this.classification,
|
errors: this.explicitErrors,
|
||||||
component: this.component,
|
allErrors: this.allErrors,
|
||||||
disableErrors: this.disableErrors,
|
formShouldShowErrors: this.formShouldShowErrors,
|
||||||
errors: this.explicitErrors,
|
getValidationErrors: this.getValidationErrors.bind(this),
|
||||||
allErrors: this.allErrors,
|
hasGivenName: this.hasGivenName,
|
||||||
formShouldShowErrors: this.formShouldShowErrors,
|
hasValidationErrors: this.hasValidationErrors.bind(this),
|
||||||
getValidationErrors: this.getValidationErrors.bind(this),
|
help: this.help,
|
||||||
hasGivenName: this.hasGivenName,
|
id: this.id || this.defaultId,
|
||||||
hasLabel: (this.label && this.classification !== 'button'),
|
imageBehavior: this.imageBehavior,
|
||||||
hasValidationErrors: this.hasValidationErrors.bind(this),
|
limit: this.limit,
|
||||||
help: this.help,
|
name: this.nameOrFallback,
|
||||||
helpPosition: this.logicalHelpPosition,
|
performValidation: this.performValidation.bind(this),
|
||||||
id: this.id || this.defaultId,
|
preventWindowDrops: this.preventWindowDrops,
|
||||||
imageBehavior: this.imageBehavior,
|
repeatable: this.repeatable,
|
||||||
label: this.label,
|
setErrors: this.setErrors.bind(this),
|
||||||
labelPosition: this.logicalLabelPosition,
|
showValidationErrors: this.showValidationErrors,
|
||||||
limit: this.limit,
|
uploadBehavior: this.uploadBehavior,
|
||||||
name: this.nameOrFallback,
|
uploadUrl: this.mergedUploadUrl,
|
||||||
performValidation: this.performValidation.bind(this),
|
uploader: this.uploader || this.$formulario.getUploader(),
|
||||||
preventWindowDrops: this.preventWindowDrops,
|
validationErrors: this.validationErrors,
|
||||||
repeatable: this.repeatable,
|
value: this.value,
|
||||||
setErrors: this.setErrors.bind(this),
|
visibleValidationErrors: this.visibleValidationErrors,
|
||||||
showValidationErrors: this.showValidationErrors,
|
})
|
||||||
slotComponents: this.slotComponents,
|
},
|
||||||
type: this.type,
|
// Used in sub-context
|
||||||
uploadBehavior: this.uploadBehavior,
|
nameOrFallback,
|
||||||
uploadUrl: this.mergedUploadUrl,
|
hasGivenName,
|
||||||
uploader: this.uploader || this.$formulate.getUploader(),
|
elementAttributes,
|
||||||
validationErrors: this.validationErrors,
|
mergedUploadUrl,
|
||||||
value: this.value,
|
|
||||||
visibleValidationErrors: this.visibleValidationErrors,
|
|
||||||
isSubField: this.isSubField,
|
|
||||||
...this.typeContext
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// Used in sub-context
|
|
||||||
nameOrFallback,
|
|
||||||
hasGivenName,
|
|
||||||
typeContext,
|
|
||||||
elementAttributes,
|
|
||||||
logicalLabelPosition,
|
|
||||||
logicalHelpPosition,
|
|
||||||
mergedUploadUrl,
|
|
||||||
|
|
||||||
// These items are not passed as context
|
// These items are not passed as context
|
||||||
isVmodeled,
|
isVmodeled,
|
||||||
mergedValidationName,
|
mergedValidationName,
|
||||||
explicitErrors,
|
explicitErrors,
|
||||||
allErrors,
|
allErrors,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
hasVisibleErrors,
|
hasVisibleErrors,
|
||||||
showValidationErrors,
|
showValidationErrors,
|
||||||
visibleValidationErrors,
|
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 {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,71 +57,35 @@ function typeContext () {
|
|||||||
* @return {object}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
function elementAttributes () {
|
function elementAttributes () {
|
||||||
const attrs = Object.assign({}, this.localAttributes)
|
const attrs = Object.assign({}, this.localAttributes)
|
||||||
// pass the ID prop through to the root element
|
// pass the ID prop through to the root element
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
attrs.id = this.id
|
attrs.id = this.id
|
||||||
} else {
|
} else {
|
||||||
attrs.id = this.defaultId
|
attrs.id = this.defaultId
|
||||||
}
|
}
|
||||||
// pass an explicitly given name prop through to the root element
|
// pass an explicitly given name prop through to the root element
|
||||||
if (this.hasGivenName) {
|
if (this.hasGivenName) {
|
||||||
attrs.name = this.name
|
attrs.name = this.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is help text, have this element be described by it.
|
// If there is help text, have this element be described by it.
|
||||||
if (this.help) {
|
if (this.help) {
|
||||||
attrs['aria-describedby'] = `${attrs.id}-help`
|
attrs['aria-describedby'] = `${attrs.id}-help`
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrs
|
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'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The validation label to use.
|
* The validation label to use.
|
||||||
*/
|
*/
|
||||||
function mergedValidationName () {
|
function mergedValidationName () {
|
||||||
if (this.validationName) {
|
if (this.validationName) {
|
||||||
return this.validationName
|
return this.validationName
|
||||||
}
|
}
|
||||||
if (typeof this.name === 'string') {
|
|
||||||
return this.name
|
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.
|
* that is defined as a plugin option.
|
||||||
*/
|
*/
|
||||||
function mergedUploadUrl () {
|
function mergedUploadUrl () {
|
||||||
return this.uploadUrl || this.$formulate.getUploadUrl()
|
return this.uploadUrl || this.$formulario.getUploadUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,13 +101,11 @@ function mergedUploadUrl () {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
function showValidationErrors () {
|
function showValidationErrors () {
|
||||||
if (this.showErrors || this.formShouldShowErrors) {
|
if (this.showErrors || this.formShouldShowErrors) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (this.classification === 'file' && this.uploadBehavior === 'live' && modelGetter.call(this)) {
|
|
||||||
return true
|
return this.behavioralErrorVisibility
|
||||||
}
|
|
||||||
return this.behavioralErrorVisibility
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -202,37 +113,35 @@ function showValidationErrors () {
|
|||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
function visibleValidationErrors () {
|
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.
|
* Return the element’s name, or select a fallback.
|
||||||
*/
|
*/
|
||||||
function nameOrFallback () {
|
function nameOrFallback () {
|
||||||
if (this.name === true && this.classification !== 'button') {
|
if (this.path !== '') {
|
||||||
return `${this.type}_${this.elementAttributes.id}`
|
return this.path + '.' + this.name
|
||||||
}
|
}
|
||||||
if (this.name === false || (this.classification === 'button' && this.name === true)) {
|
|
||||||
return false
|
return this.name
|
||||||
}
|
|
||||||
return this.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* determine if an input has a user-defined name
|
* determine if an input has a user-defined name
|
||||||
*/
|
*/
|
||||||
function hasGivenName () {
|
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 () {
|
function isVmodeled () {
|
||||||
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
|
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
|
||||||
this._events &&
|
this._events &&
|
||||||
Array.isArray(this._events.input) &&
|
Array.isArray(this._events.input) &&
|
||||||
this._events.input.length)
|
this._events.input.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,70 +151,56 @@ function isVmodeled () {
|
|||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
function createOptionList (options) {
|
function createOptionList (options) {
|
||||||
if (!Array.isArray(options) && options && typeof options === 'object') {
|
if (!Array.isArray(options) && options && typeof options === 'object') {
|
||||||
const optionList = []
|
const optionList = []
|
||||||
const that = this
|
const that = this
|
||||||
for (const value in options) {
|
for (const value in options) {
|
||||||
optionList.push({ value, label: options[value], id: `${that.elementAttributes.id}_${value}` })
|
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.
|
* These are errors we that have been explicity passed to us.
|
||||||
*/
|
*/
|
||||||
function explicitErrors () {
|
function explicitErrors () {
|
||||||
return arrayify(this.errors)
|
return arrayify(this.errors)
|
||||||
.concat(this.localErrors)
|
.concat(this.localErrors)
|
||||||
.concat(arrayify(this.error))
|
.concat(arrayify(this.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The merged errors computed property.
|
* The merged errors computed property.
|
||||||
*/
|
*/
|
||||||
function allErrors () {
|
function allErrors () {
|
||||||
return this.explicitErrors
|
return this.explicitErrors
|
||||||
.concat(arrayify(this.validationErrors))
|
.concat(arrayify(this.validationErrors))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does this computed property have errors
|
* Does this computed property have errors
|
||||||
*/
|
*/
|
||||||
function hasErrors () {
|
function hasErrors () {
|
||||||
return !!this.allErrors.length
|
return !!this.allErrors.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if form has actively visible errors (of any kind)
|
* Returns if form has actively visible errors (of any kind)
|
||||||
*/
|
*/
|
||||||
function hasVisibleErrors () {
|
function hasVisibleErrors () {
|
||||||
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
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')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bound into the context object.
|
* Bound into the context object.
|
||||||
*/
|
*/
|
||||||
function blurHandler () {
|
function blurHandler () {
|
||||||
this.$emit('blur')
|
this.$emit('blur')
|
||||||
if (this.errorBehavior === 'blur') {
|
if (this.errorBehavior === 'blur') {
|
||||||
this.behavioralErrorVisibility = true
|
this.behavioralErrorVisibility = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -313,35 +208,32 @@ function blurHandler () {
|
|||||||
* @param {object} context
|
* @param {object} context
|
||||||
*/
|
*/
|
||||||
function defineModel (context) {
|
function defineModel (context) {
|
||||||
return Object.defineProperty(context, 'model', {
|
return Object.defineProperty(context, 'model', {
|
||||||
get: modelGetter.bind(this),
|
get: modelGetter.bind(this),
|
||||||
set: modelSetter.bind(this)
|
set: modelSetter.bind(this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value from a model.
|
* Get the value from a model.
|
||||||
**/
|
**/
|
||||||
function modelGetter () {
|
function modelGetter () {
|
||||||
const model = this.isVmodeled ? 'formulateValue' : 'proxy'
|
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
|
||||||
if (this.type === 'checkbox' && !Array.isArray(this[model]) && this.options) {
|
if (this[model] === undefined) {
|
||||||
return []
|
return ''
|
||||||
}
|
}
|
||||||
if (!this[model]) {
|
return this[model]
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return this[model]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value from a model.
|
* Set the value from a model.
|
||||||
**/
|
**/
|
||||||
function modelSetter (value) {
|
function modelSetter (value) {
|
||||||
if (!shallowEqualObjects(value, this.proxy)) {
|
if (!shallowEqualObjects(value, this.proxy)) {
|
||||||
this.proxy = value
|
this.proxy = value
|
||||||
}
|
}
|
||||||
this.$emit('input', value)
|
this.$emit('input', value)
|
||||||
if (this.context.name && typeof this.formulateSetter === 'function') {
|
if (this.context.name && typeof this.formularioSetter === 'function') {
|
||||||
this.formulateSetter(this.context.name, value)
|
this.formularioSetter(this.context.name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,27 +7,27 @@
|
|||||||
* @param {object} options
|
* @param {object} options
|
||||||
*/
|
*/
|
||||||
export default function (file, progress, error, options) {
|
export default function (file, progress, error, options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
|
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a recursive timeout that advances the progress.
|
* Create a recursive timeout that advances the progress.
|
||||||
*/
|
*/
|
||||||
const advance = () => setTimeout(() => {
|
const advance = () => setTimeout(() => {
|
||||||
const elapsed = performance.now() - start
|
const elapsed = performance.now() - start
|
||||||
const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100))
|
const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100))
|
||||||
progress(currentProgress)
|
progress(currentProgress)
|
||||||
|
|
||||||
if (currentProgress >= 100) {
|
if (currentProgress >= 100) {
|
||||||
return resolve({
|
return resolve({
|
||||||
url: 'http://via.placeholder.com/350x150.png',
|
url: 'http://via.placeholder.com/350x150.png',
|
||||||
name: file.name
|
name: file.name
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
advance()
|
||||||
|
}
|
||||||
|
}, 20)
|
||||||
advance()
|
advance()
|
||||||
}
|
})
|
||||||
}, 20)
|
|
||||||
advance()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
* on the documentation site vueformulate.com.
|
* on the documentation site vueformulate.com.
|
||||||
*/
|
*/
|
||||||
export default function (err) {
|
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
|
* 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.
|
* overly terse for that reason alone, we wouldn't necessarily recommend this.
|
||||||
*/
|
*/
|
||||||
const fi = 'FormulateInput'
|
const fi = 'FormularioInput'
|
||||||
const add = (n, c) => ({
|
const add = (n, c) => ({
|
||||||
classification: n,
|
classification: n,
|
||||||
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))
|
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))
|
||||||
})
|
})
|
||||||
export default {
|
export default {
|
||||||
// === SINGLE LINE TEXT STYLE INPUTS
|
// === SINGLE LINE TEXT STYLE INPUTS
|
||||||
...[
|
...[
|
||||||
'text',
|
'text',
|
||||||
'email',
|
'email',
|
||||||
'number',
|
'number',
|
||||||
'color',
|
'color',
|
||||||
'date',
|
'date',
|
||||||
'hidden',
|
'hidden',
|
||||||
'month',
|
'month',
|
||||||
'password',
|
'password',
|
||||||
'search',
|
'search',
|
||||||
'tel',
|
'tel',
|
||||||
'time',
|
'time',
|
||||||
'url',
|
'url',
|
||||||
'week',
|
'week',
|
||||||
'datetime-local'
|
'datetime-local'
|
||||||
].reduce((lib, type) => ({ ...lib, [type]: add('text') }), {}),
|
].reduce((lib, type) => ({ ...lib, [type]: add('text') }), {}),
|
||||||
|
|
||||||
// === SLIDER INPUTS
|
// === SLIDER INPUTS
|
||||||
range: add('slider'),
|
range: add('slider'),
|
||||||
|
|
||||||
// === MULTI LINE TEXT INPUTS
|
// === MULTI LINE TEXT INPUTS
|
||||||
textarea: add('textarea', 'TextArea'),
|
textarea: add('textarea', 'TextArea'),
|
||||||
|
|
||||||
// === BOX STYLE INPUTS
|
// === BOX STYLE INPUTS
|
||||||
checkbox: add('box'),
|
checkbox: add('box'),
|
||||||
radio: add('box'),
|
radio: add('box'),
|
||||||
|
|
||||||
// === BUTTON STYLE INPUTS
|
// === BUTTON STYLE INPUTS
|
||||||
submit: add('button'),
|
submit: add('button'),
|
||||||
button: add('button'),
|
button: add('button'),
|
||||||
|
|
||||||
// === SELECT STYLE INPUTS
|
// === SELECT STYLE INPUTS
|
||||||
select: add('select'),
|
select: add('select'),
|
||||||
|
|
||||||
// === FILE TYPE
|
// === FILE TYPE
|
||||||
file: add('file'),
|
file: add('file'),
|
||||||
image: add('file'),
|
image: add('file'),
|
||||||
|
|
||||||
// === GROUP TYPE
|
// === GROUP TYPE
|
||||||
group: add('group')
|
group: add('group')
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const i = 'image/'
|
const i = 'image/'
|
||||||
export default {
|
export default {
|
||||||
'csv': 'text/csv',
|
'csv': 'text/csv',
|
||||||
'gif': i + 'gif',
|
'gif': i + 'gif',
|
||||||
'jpg': i + 'jpeg',
|
'jpg': i + 'jpeg',
|
||||||
'jpeg': i + 'jpeg',
|
'jpeg': i + 'jpeg',
|
||||||
'png': i + 'png',
|
'png': i + 'png',
|
||||||
'pdf': 'application/pdf',
|
'pdf': 'application/pdf',
|
||||||
'svg': i + 'svg+xml'
|
'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
|
* Component registry with inherent depth to handle complex nesting. This is
|
||||||
* important for features such as grouped fields.
|
* important for features such as grouped fields.
|
||||||
*/
|
*/
|
||||||
class Registry {
|
class Registry {
|
||||||
/**
|
/**
|
||||||
* Create a new registry of components.
|
* Create a new registry of components.
|
||||||
* @param {vm} ctx The host vm context of the registry.
|
* @param {vm} ctx The host vm context of the registry.
|
||||||
*/
|
*/
|
||||||
constructor (ctx) {
|
constructor (ctx) {
|
||||||
this.registry = new Map()
|
this.registry = new Map()
|
||||||
this.ctx = ctx
|
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
|
|
||||||
}
|
}
|
||||||
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.
|
* Add an item to the registry.
|
||||||
* @param {function} callback
|
* @param {string|array} key
|
||||||
*/
|
* @param {vue} component
|
||||||
reduce (callback, accumulator) {
|
*/
|
||||||
this.registry.forEach((component, field) => {
|
add (name, component) {
|
||||||
accumulator = callback(accumulator, component, field)
|
this.registry.set(name, component)
|
||||||
})
|
return this
|
||||||
return accumulator
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Remove an item from the registry.
|
||||||
* Data props to expose.
|
* @param {string} name
|
||||||
*/
|
*/
|
||||||
dataProps () {
|
remove (name) {
|
||||||
return {
|
this.registry.delete(name)
|
||||||
proxy: {},
|
const { [name]: value, ...newProxy } = this.ctx.proxy
|
||||||
registry: this,
|
this.ctx.proxy = newProxy
|
||||||
register: this.register.bind(this),
|
return this
|
||||||
deregister: field => this.remove(field),
|
}
|
||||||
childrenShouldShowErrors: false
|
|
||||||
|
/**
|
||||||
|
* 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
|
* @param {component} contextComponent
|
||||||
*/
|
*/
|
||||||
export default function useRegistry (contextComponent) {
|
export default function useRegistry (contextComponent) {
|
||||||
const registry = new Registry(contextComponent)
|
const registry = new Registry(contextComponent)
|
||||||
return registry.dataProps()
|
return registry.dataProps()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed properties related to the registry.
|
* Computed properties related to the registry.
|
||||||
*/
|
*/
|
||||||
export function useRegistryComputed () {
|
export function useRegistryComputed () {
|
||||||
return {
|
return {
|
||||||
hasInitialValue () {
|
hasInitialValue () {
|
||||||
return (
|
return (
|
||||||
(this.formulateValue && typeof this.formulateValue === 'object') ||
|
(this.formularioValue && typeof this.formularioValue === 'object') ||
|
||||||
(this.values && typeof this.values === 'object') ||
|
(this.values && typeof this.values === 'object') ||
|
||||||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isVmodeled () {
|
isVmodeled () {
|
||||||
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
|
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
|
||||||
this._events &&
|
this._events &&
|
||||||
Array.isArray(this._events.input) &&
|
Array.isArray(this._events.input) &&
|
||||||
this._events.input.length)
|
this._events.input.length)
|
||||||
},
|
},
|
||||||
initialValues () {
|
initialValues () {
|
||||||
if (
|
if (
|
||||||
has(this.$options.propsData, 'formulateValue') &&
|
has(this.$options.propsData, 'formularioValue') &&
|
||||||
typeof this.formulateValue === 'object'
|
typeof this.formularioValue === 'object'
|
||||||
) {
|
) {
|
||||||
// If there is a v-model on the form/group, use those values as first priority
|
// If there is a v-model on the form/group, use those values as first priority
|
||||||
return Object.assign({}, this.formulateValue) // @todo - use a deep clone to detach reference types
|
return Object.assign({}, this.formularioValue) // @todo - use a deep clone to detach reference types
|
||||||
} else if (
|
} else if (
|
||||||
has(this.$options.propsData, 'values') &&
|
has(this.$options.propsData, 'values') &&
|
||||||
typeof this.values === 'object'
|
typeof this.values === 'object'
|
||||||
) {
|
) {
|
||||||
// If there are values, use them as secondary priority
|
// If there are values, use them as secondary priority
|
||||||
return Object.assign({}, this.values)
|
return Object.assign({}, this.values)
|
||||||
} else if (
|
} else if (
|
||||||
this.isGrouping && typeof this.context.model[this.index] === 'object'
|
this.isGrouping && typeof this.context.model[this.index] === 'object'
|
||||||
) {
|
) {
|
||||||
return this.context.model[this.index]
|
return this.context.model[this.index]
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods used in the registry.
|
* Methods used in the registry.
|
||||||
*/
|
*/
|
||||||
export function useRegistryMethods (without = []) {
|
export function useRegistryMethods (without = []) {
|
||||||
const methods = {
|
const methods = {
|
||||||
applyInitialValues () {
|
applyInitialValues () {
|
||||||
if (this.hasInitialValue) {
|
if (this.hasInitialValue) {
|
||||||
this.proxy = this.initialValues
|
this.proxy = this.initialValues
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setFieldValue (field, value) {
|
setFieldValue (field, value) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
const { [field]: value, ...proxy } = this.proxy
|
const { [field]: value, ...proxy } = this.proxy
|
||||||
this.proxy = proxy
|
this.proxy = proxy
|
||||||
} else {
|
} else {
|
||||||
Object.assign(this.proxy, { [field]: value })
|
setNested(this.proxy, field, value);
|
||||||
}
|
}
|
||||||
this.$emit('input', Object.assign({}, this.proxy))
|
this.$emit('input', Object.assign({}, this.proxy))
|
||||||
},
|
},
|
||||||
getFormValues () {
|
getFormValues () {
|
||||||
return this.proxy
|
return this.proxy
|
||||||
},
|
},
|
||||||
hasValidationErrors () {
|
hasValidationErrors () {
|
||||||
return Promise.all(this.registry.reduce((resolvers, cmp, name) => {
|
return Promise.all(this.registry.reduce((resolvers, cmp, name) => {
|
||||||
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
|
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
|
||||||
return resolvers
|
return resolvers
|
||||||
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
|
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
|
||||||
},
|
},
|
||||||
showErrors () {
|
showErrors () {
|
||||||
this.childrenShouldShowErrors = true
|
this.childrenShouldShowErrors = true
|
||||||
this.registry.map(input => {
|
this.registry.map(input => {
|
||||||
input.formShouldShowErrors = true
|
input.formShouldShowErrors = true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
hideErrors () {
|
hideErrors () {
|
||||||
this.childrenShouldShowErrors = false
|
this.childrenShouldShowErrors = false
|
||||||
this.registry.map(input => {
|
this.registry.map(input => {
|
||||||
input.formShouldShowErrors = false
|
input.formShouldShowErrors = false
|
||||||
input.behavioralErrorVisibility = false
|
input.behavioralErrorVisibility = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setValues (values) {
|
setValues (values) {
|
||||||
// Collect all keys, existing and incoming
|
// Collect all keys, existing and incoming
|
||||||
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
|
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
|
||||||
keys.forEach(field => {
|
keys.forEach(field => {
|
||||||
if (this.registry.has(field) &&
|
if (this.registry.has(field) &&
|
||||||
!shallowEqualObjects(values[field], this.proxy[field]) &&
|
!shallowEqualObjects(getNested(values, field), getNested(this.proxy, field)) &&
|
||||||
!shallowEqualObjects(values[field], this.registry.get(field).proxy)
|
!shallowEqualObjects(getNested(values, field), this.registry.get(field).proxy)
|
||||||
) {
|
) {
|
||||||
this.setFieldValue(field, values[field])
|
this.setFieldValue(field, getNested(values, field))
|
||||||
this.registry.get(field).context.model = values[field]
|
this.registry.get(field).context.model = getNested(values, field)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
return Object.keys(methods).reduce((withMethods, key) => {
|
||||||
return Object.keys(methods).reduce((withMethods, key) => {
|
return without.includes(key) ? withMethods : { ...withMethods, [key]: methods[key] }
|
||||||
return without.includes(key) ? withMethods : { ...withMethods, [key]: methods[key] }
|
}, {})
|
||||||
}, {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Providers related to the registry.
|
* Providers related to the registry.
|
||||||
*/
|
*/
|
||||||
export function useRegistryProviders (ctx) {
|
export function useRegistryProviders (ctx) {
|
||||||
return {
|
return {
|
||||||
formulateSetter: ctx.setFieldValue,
|
formularioSetter: ctx.setFieldValue,
|
||||||
formulateRegister: ctx.register,
|
formularioRegister: ctx.register,
|
||||||
formulateDeregister: ctx.deregister,
|
formularioDeregister: ctx.deregister,
|
||||||
getFormValues: ctx.getFormValues
|
getFormValues: ctx.getFormValues
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,287 +6,287 @@ import { shallowEqualObjects, regexForFormat } from './utils'
|
|||||||
* Library of rules
|
* Library of rules
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* Rule: the value must be "yes", "on", "1", or true
|
* Rule: the value must be "yes", "on", "1", or true
|
||||||
*/
|
*/
|
||||||
accepted: function ({ value }) {
|
accepted: function ({ value }) {
|
||||||
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
|
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule: checks if a value is after a given date. Defaults to current time
|
* Rule: checks if a value is after a given date. Defaults to current time
|
||||||
*/
|
*/
|
||||||
after: function ({ value }, compare = false) {
|
after: function ({ value }, compare = false) {
|
||||||
const timestamp = Date.parse(compare || new Date())
|
const timestamp = Date.parse(compare || new Date())
|
||||||
const fieldValue = Date.parse(value)
|
const fieldValue = Date.parse(value)
|
||||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule: checks if the value is only alpha
|
* Rule: checks if the value is only alpha
|
||||||
*/
|
*/
|
||||||
alpha: function ({ value }, set = 'default') {
|
alpha: function ({ value }, set = 'default') {
|
||||||
const sets = {
|
const sets = {
|
||||||
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
||||||
latin: /^[a-zA-Z]+$/
|
latin: /^[a-zA-Z]+$/
|
||||||
}
|
|
||||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||||
return true
|
return Promise.resolve(sets[selectedSet].test(value))
|
||||||
})())
|
},
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the minimum value of a particular.
|
* Rule: checks if the value is alpha numeric
|
||||||
*/
|
*/
|
||||||
min: function ({ value }, minimum = 1, force) {
|
alphanumeric: function ({ value }, set = 'default') {
|
||||||
return Promise.resolve((() => {
|
const sets = {
|
||||||
if (Array.isArray(value)) {
|
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
|
||||||
minimum = !isNaN(minimum) ? Number(minimum) : minimum
|
latin: /^[a-zA-Z0-9]+$/
|
||||||
return value.length >= minimum
|
}
|
||||||
}
|
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
return Promise.resolve(sets[selectedSet].test(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.
|
* Rule: checks if a value is after a given date. Defaults to current time
|
||||||
*/
|
*/
|
||||||
max: function ({ value }, maximum = 10, force) {
|
before: function ({ value }, compare = false) {
|
||||||
return Promise.resolve((() => {
|
const timestamp = Date.parse(compare || new Date())
|
||||||
if (Array.isArray(value)) {
|
const fieldValue = Date.parse(value)
|
||||||
maximum = !isNaN(maximum) ? Number(maximum) : maximum
|
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
|
||||||
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.
|
* Rule: checks if the value is between two other values
|
||||||
*/
|
*/
|
||||||
not: function ({ value }, ...stack) {
|
between: function ({ value }, from = 0, to = 10, force) {
|
||||||
return Promise.resolve(stack.find(item => {
|
return Promise.resolve((() => {
|
||||||
if (typeof item === 'object') {
|
if (from === null || to === null || isNaN(from) || isNaN(to)) {
|
||||||
return shallowEqualObjects(item, value)
|
return false
|
||||||
}
|
}
|
||||||
return item === value
|
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||||
}) === undefined)
|
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
|
* Confirm that the value of one field is the same as another, mostly used
|
||||||
*/
|
* for password confirmations.
|
||||||
number: function ({ value }) {
|
*/
|
||||||
return Promise.resolve(!isNaN(value))
|
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
|
* Rule: ensures the value is a date according to Date.parse(), or a format
|
||||||
*/
|
* regex.
|
||||||
required: function ({ value }, isRequired = true) {
|
*/
|
||||||
return Promise.resolve((() => {
|
date: function ({ value }, format = false) {
|
||||||
if (!isRequired || ['no', 'false'].includes(isRequired)) {
|
return Promise.resolve((() => {
|
||||||
return true
|
if (format && typeof format === 'string') {
|
||||||
}
|
return regexForFormat(format).test(value)
|
||||||
if (Array.isArray(value)) {
|
}
|
||||||
return !!value.length
|
return !isNaN(Date.parse(value))
|
||||||
}
|
})())
|
||||||
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
|
* Rule: tests
|
||||||
*/
|
*/
|
||||||
startsWith: function ({ value }, ...stack) {
|
email: function ({ value }) {
|
||||||
return Promise.resolve((() => {
|
// eslint-disable-next-line
|
||||||
if (typeof value === 'string' && stack.length) {
|
const isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||||
return stack.find(item => {
|
return Promise.resolve(isEmail.test(value))
|
||||||
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
|
* Rule: Value ends with one of the given Strings
|
||||||
*/
|
*/
|
||||||
url: function ({ value }) {
|
endsWith: function ({ value }, ...stack) {
|
||||||
return Promise.resolve(isUrl(value))
|
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.
|
* Rule: Value is in an array (stack).
|
||||||
*/
|
*/
|
||||||
bail: function () {
|
in: function ({ value }, ...stack) {
|
||||||
return Promise.resolve(true)
|
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
|
* @param {Function} callback
|
||||||
*/
|
*/
|
||||||
export function map (original, callback) {
|
export function map (original, callback) {
|
||||||
const obj = {}
|
const obj = {}
|
||||||
for (let key in original) {
|
for (let key in original) {
|
||||||
obj[key] = callback(key, original[key])
|
obj[key] = callback(key, original[key])
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,28 +19,28 @@ export function map (original, callback) {
|
|||||||
* @param {*} objB
|
* @param {*} objB
|
||||||
*/
|
*/
|
||||||
export function shallowEqualObjects (objA, objB) {
|
export function shallowEqualObjects (objA, objB) {
|
||||||
if (objA === objB) {
|
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
|
|
||||||
}
|
}
|
||||||
}
|
if (!objA || !objB) {
|
||||||
return true
|
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
|
* @param {String} string
|
||||||
*/
|
*/
|
||||||
export function snakeToCamel (string) {
|
export function snakeToCamel (string) {
|
||||||
if (typeof string === 'string') {
|
if (typeof string === 'string') {
|
||||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||||
return $1.toUpperCase().replace('_', '')
|
return $1.toUpperCase().replace('_', '')
|
||||||
}
|
}
|
||||||
return $1
|
return $1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,19 +64,19 @@ export function snakeToCamel (string) {
|
|||||||
* @param {mixed} item
|
* @param {mixed} item
|
||||||
*/
|
*/
|
||||||
export function arrayify (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 []
|
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
|
* @return {array} an array of functions
|
||||||
*/
|
*/
|
||||||
export function parseRules (validation, rules) {
|
export function parseRules (validation, rules) {
|
||||||
if (typeof validation === 'string') {
|
if (typeof validation === 'string') {
|
||||||
return parseRules(validation.split('|'), rules)
|
return parseRules(validation.split('|'), rules)
|
||||||
}
|
}
|
||||||
if (!Array.isArray(validation)) {
|
if (!Array.isArray(validation)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return validation.map(rule => parseRule(rule, rules)).filter(f => !!f)
|
return validation.map(rule => parseRule(rule, rules)).filter(f => !!f)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,29 +101,29 @@ export function parseRules (validation, rules) {
|
|||||||
* @param {string|function} rule
|
* @param {string|function} rule
|
||||||
*/
|
*/
|
||||||
function parseRule (rule, rules) {
|
function parseRule (rule, rules) {
|
||||||
if (typeof rule === 'function') {
|
if (typeof rule === 'function') {
|
||||||
return [rule, []]
|
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 ruleName === 'function') {
|
if (Array.isArray(rule) && rule.length) {
|
||||||
return [ruleName, rule, ruleName, modifier]
|
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') {
|
||||||
if (typeof rule === 'string') {
|
const segments = rule.split(':')
|
||||||
const segments = rule.split(':')
|
const [ruleName, modifier] = parseModifier(segments.shift())
|
||||||
const [ruleName, modifier] = parseModifier(segments.shift())
|
if (rules.hasOwnProperty(ruleName)) {
|
||||||
if (rules.hasOwnProperty(ruleName)) {
|
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
||||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
} else {
|
||||||
} else {
|
throw new Error(`Unknown validation rule ${rule}`)
|
||||||
throw new Error(`Unknown validation rule ${rule}`)
|
}
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,10 +132,10 @@ function parseRule (rule, rules) {
|
|||||||
* @return {array} [ruleName, modifier]
|
* @return {array} [ruleName, modifier]
|
||||||
*/
|
*/
|
||||||
function parseModifier (ruleName) {
|
function parseModifier (ruleName) {
|
||||||
if (/^[\^]/.test(ruleName.charAt(0))) {
|
if (/^[\^]/.test(ruleName.charAt(0))) {
|
||||||
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
||||||
}
|
}
|
||||||
return [snakeToCamel(ruleName), null]
|
return [snakeToCamel(ruleName), null]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,40 +152,40 @@ function parseModifier (ruleName) {
|
|||||||
* @param {array} rules
|
* @param {array} rules
|
||||||
*/
|
*/
|
||||||
export function groupBails (rules) {
|
export function groupBails (rules) {
|
||||||
const groups = []
|
const groups = []
|
||||||
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||||
if (bailIndex >= 0) {
|
if (bailIndex >= 0) {
|
||||||
// Get all the rules until the first bail rule (dont include the bail)
|
// Get all the rules until the first bail rule (dont include the bail)
|
||||||
const preBail = rules.splice(0, bailIndex + 1).slice(0, -1)
|
const preBail = rules.splice(0, bailIndex + 1).slice(0, -1)
|
||||||
// Rules before the `bail` rule are non-bailing
|
// Rules before the `bail` rule are non-bailing
|
||||||
preBail.length && groups.push(preBail)
|
preBail.length && groups.push(preBail)
|
||||||
// All remaining rules are bailing rule groups
|
// All remaining rules are bailing rule groups
|
||||||
rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true })))
|
rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true })))
|
||||||
} else {
|
} else {
|
||||||
groups.push(rules)
|
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
|
|
||||||
}
|
}
|
||||||
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
|
* @param {string} string
|
||||||
*/
|
*/
|
||||||
export function escapeRegExp (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
|
* @param {string} format
|
||||||
*/
|
*/
|
||||||
export function regexForFormat (format) {
|
export function regexForFormat (format) {
|
||||||
let escaped = `^${escapeRegExp(format)}$`
|
let escaped = `^${escapeRegExp(format)}$`
|
||||||
const formats = {
|
const formats = {
|
||||||
MM: '(0[1-9]|1[012])',
|
MM: '(0[1-9]|1[012])',
|
||||||
M: '([1-9]|1[012])',
|
M: '([1-9]|1[012])',
|
||||||
DD: '([012][1-9]|3[01])',
|
DD: '([012][1-9]|3[01])',
|
||||||
D: '([012]?[1-9]|3[01])',
|
D: '([012]?[1-9]|3[01])',
|
||||||
YYYY: '\\d{4}',
|
YYYY: '\\d{4}',
|
||||||
YY: '\\d{2}'
|
YY: '\\d{2}'
|
||||||
}
|
}
|
||||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||||
return regex.replace(format, formats[format])
|
return regex.replace(format, formats[format])
|
||||||
}, escaped))
|
}, escaped))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,19 +220,19 @@ export function regexForFormat (format) {
|
|||||||
* @param {mixed} data
|
* @param {mixed} data
|
||||||
*/
|
*/
|
||||||
export function isValueType (data) {
|
export function isValueType (data) {
|
||||||
switch (typeof data) {
|
switch (typeof data) {
|
||||||
case 'symbol':
|
case 'symbol':
|
||||||
case 'number':
|
case 'number':
|
||||||
case 'string':
|
case 'string':
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,19 +240,19 @@ export function isValueType (data) {
|
|||||||
* case of needing to unbind reactive watchers.
|
* case of needing to unbind reactive watchers.
|
||||||
*/
|
*/
|
||||||
export function cloneDeep (obj) {
|
export function cloneDeep (obj) {
|
||||||
if (typeof obj !== 'object') {
|
if (typeof obj !== 'object') {
|
||||||
return obj
|
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])
|
|
||||||
}
|
}
|
||||||
}
|
const isArr = Array.isArray(obj)
|
||||||
return newObj
|
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
|
* @param {string} locale
|
||||||
*/
|
*/
|
||||||
export function parseLocale (locale) {
|
export function parseLocale (locale) {
|
||||||
const segments = locale.split('-')
|
const segments = locale.split('-')
|
||||||
return segments.reduce((options, segment) => {
|
return segments.reduce((options, segment) => {
|
||||||
if (options.length) {
|
if (options.length) {
|
||||||
options.unshift(`${options[0]}-${segment}`)
|
options.unshift(`${options[0]}-${segment}`)
|
||||||
}
|
}
|
||||||
return options.length ? options : [segment]
|
return options.length ? options : [segment]
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||||
*/
|
*/
|
||||||
export function has (ctx, prop) {
|
export function has (ctx, 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
|
* @param {Symbol} id
|
||||||
*/
|
*/
|
||||||
export function setId (o, 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