1
0
mirror of synced 2024-11-23 22:06:07 +03:00

Library renamed to formulario

This commit is contained in:
1on 2020-05-22 14:22:56 +03:00
parent 8651cd9d30
commit 83d36526c3
36 changed files with 2108 additions and 2843 deletions

View File

@ -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%

View File

@ -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

View File

@ -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
} }

View File

@ -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

View File

@ -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" }
}
} }

View File

@ -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

View File

@ -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
View 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 its 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
View 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
View 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>

View 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
View 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>

View File

@ -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 its 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()

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}
}
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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>

View File

@ -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 elements name, or select a fallback. * Return the elements 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)
} }
} }

View File

@ -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()
})
} }

View File

@ -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 {}
} }

View File

@ -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')
} }

View File

@ -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'
} }

View File

@ -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
} }
} }

View File

@ -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)
}
} }

View File

@ -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;
} }

View File

@ -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).

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>