Library renamed to formulario
This commit is contained in:
parent
8651cd9d30
commit
83d36526c3
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -10,5 +10,5 @@ assignees: ''
|
|||||||
**Describe the new feature you'd like**
|
**Describe the new feature you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**What percentage of vue-formulate users would benefit?**
|
**What percentage of vue-formulario users would benefit?**
|
||||||
80%
|
80%
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 RetailDriver LLC
|
||||||
Copyright (c) 2020 Braid LLC
|
Copyright (c) 2020 Braid LLC
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
@ -5,16 +5,15 @@ import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
|||||||
import { terser } from 'rollup-plugin-terser'
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/Formulate.js', // Path relative to package.json
|
input: 'src/Formulario.js', // Path relative to package.json
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: 'Formulate',
|
name: 'Formulario',
|
||||||
exports: 'default',
|
exports: 'default',
|
||||||
globals: {
|
globals: {
|
||||||
'is-plain-object': 'isPlainObject',
|
'is-plain-object': 'isPlainObject',
|
||||||
'nanoid/non-secure': 'nanoid',
|
'nanoid/non-secure': 'nanoid',
|
||||||
'is-url': 'isUrl',
|
'is-url': 'isUrl',
|
||||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
|
||||||
},
|
},
|
||||||
sourcemap: false
|
sourcemap: false
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,15 @@ import internal from 'rollup-plugin-internal'
|
|||||||
import { terser } from 'rollup-plugin-terser'
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/Formulate.js', // Path relative to package.json
|
input: 'src/Formulario.js', // Path relative to package.json
|
||||||
output: {
|
output: {
|
||||||
name: 'VueFormulate',
|
name: 'VueFormulario',
|
||||||
exports: 'default',
|
exports: 'default',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
globals: {
|
globals: {
|
||||||
'is-plain-object': 'isPlainObject',
|
'is-plain-object': 'isPlainObject',
|
||||||
'nanoid/non-secure': 'nanoid',
|
'nanoid/non-secure': 'nanoid',
|
||||||
'is-url': 'isUrl',
|
'is-url': 'isUrl',
|
||||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -24,7 +23,7 @@ export default {
|
|||||||
preferBuiltins: false
|
preferBuiltins: false
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url', '@braid/vue-formulate-i18n']),
|
internal(['is-plain-object', 'nanoid/non-secure', 'is-url']),
|
||||||
vue({
|
vue({
|
||||||
css: true, // Dynamically inject css as a <style> tag
|
css: true, // Dynamically inject css as a <style> tag
|
||||||
compileTemplate: true // Explicitly convert template to render function
|
compileTemplate: true // Explicitly convert template to render function
|
||||||
|
35
package.json
35
package.json
@ -1,45 +1,45 @@
|
|||||||
{
|
{
|
||||||
"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/Formulate.js"
|
"./sfc": "src/Formulario.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulate.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulate.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulate.min.js | wc -c)b gzip\"",
|
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulario.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulario.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulario.min.js | wc -c)b gzip\"",
|
||||||
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulate.esm.js",
|
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js",
|
||||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulate.umd.js",
|
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
|
||||||
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulate.min.js",
|
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js",
|
||||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
"test": "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/formulate.esm.js | wc -c",
|
"build:size": "gzip -c dist/formulario.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/wearebraid/vue-formulate.git"
|
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"vue",
|
"vue",
|
||||||
"form",
|
"form",
|
||||||
"forms",
|
"forms",
|
||||||
"validation",
|
"validation",
|
||||||
"vuex",
|
|
||||||
"validate"
|
"validate"
|
||||||
],
|
],
|
||||||
"author": "Justin Schroeder <justin@wearebraid.com>",
|
"author": "RetailDriverLLC <integration@retailcrm.ru>",
|
||||||
|
"contributors": [
|
||||||
|
"Justin Schroeder <justin@wearebraid.com>"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/wearebraid/vue-formulate/issues"
|
"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",
|
||||||
@ -81,7 +81,6 @@
|
|||||||
"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"
|
||||||
|
@ -3,8 +3,8 @@ 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
|
||||||
|
276
src/Formulario.js
Normal file
276
src/Formulario.js
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import library from './libs/library'
|
||||||
|
import rules from './libs/rules'
|
||||||
|
import mimes from './libs/mimes'
|
||||||
|
import FileUpload from './FileUpload'
|
||||||
|
import { arrayify, parseLocale, has } from './libs/utils'
|
||||||
|
import isPlainObject from 'is-plain-object'
|
||||||
|
import fauxUploader from './libs/faux-uploader'
|
||||||
|
import FormularioForm from './FormularioForm.vue'
|
||||||
|
import FormularioInput from './FormularioInput.vue'
|
||||||
|
import FormularioGrouping from './FormularioGrouping.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base formulario library.
|
||||||
|
*/
|
||||||
|
class Formulario {
|
||||||
|
/**
|
||||||
|
* Instantiate our base options.
|
||||||
|
*/
|
||||||
|
constructor () {
|
||||||
|
this.options = {}
|
||||||
|
this.defaults = {
|
||||||
|
components: {
|
||||||
|
FormularioForm,
|
||||||
|
FormularioInput,
|
||||||
|
FormularioGrouping,
|
||||||
|
},
|
||||||
|
library,
|
||||||
|
rules,
|
||||||
|
mimes,
|
||||||
|
locale: false,
|
||||||
|
uploader: fauxUploader,
|
||||||
|
uploadUrl: false,
|
||||||
|
fileUrlKey: 'url',
|
||||||
|
uploadJustCompleteDuration: 1000,
|
||||||
|
errorHandler: (err) => err,
|
||||||
|
plugins: [],
|
||||||
|
idPrefix: 'formulario-'
|
||||||
|
}
|
||||||
|
this.registry = new Map()
|
||||||
|
this.idRegistry = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install vue formulario, and register it’s components.
|
||||||
|
*/
|
||||||
|
install (Vue, options) {
|
||||||
|
Vue.prototype.$formulario = this
|
||||||
|
this.options = this.defaults
|
||||||
|
var plugins = this.defaults.plugins
|
||||||
|
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
||||||
|
plugins = plugins.concat(options.plugins)
|
||||||
|
}
|
||||||
|
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||||
|
this.extend(options || {})
|
||||||
|
for (var componentName in this.options.components) {
|
||||||
|
Vue.component(componentName, this.options.components[componentName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce a deterministically generated id based on the sequence by which it
|
||||||
|
* was requested. This should be *theoretically* the same SSR as client side.
|
||||||
|
* However, SSR and deterministic ids can be very challenging, so this
|
||||||
|
* implementation is open to community review.
|
||||||
|
*/
|
||||||
|
nextId (vm) {
|
||||||
|
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
||||||
|
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
|
||||||
|
this.idRegistry[pathPrefix] = 0
|
||||||
|
}
|
||||||
|
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a set of options, apply them to the pre-existing options.
|
||||||
|
* @param {Object} extendWith
|
||||||
|
*/
|
||||||
|
extend (extendWith) {
|
||||||
|
if (typeof extendWith === 'object') {
|
||||||
|
this.options = this.merge(this.options, extendWith)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new object by copying properties of base and mergeWith.
|
||||||
|
* Note: arrays don't overwrite - they push
|
||||||
|
*
|
||||||
|
* @param {Object} base
|
||||||
|
* @param {Object} mergeWith
|
||||||
|
* @param {boolean} concatArrays
|
||||||
|
*/
|
||||||
|
merge (base, mergeWith, concatArrays = true) {
|
||||||
|
var merged = {}
|
||||||
|
for (var key in base) {
|
||||||
|
if (mergeWith.hasOwnProperty(key)) {
|
||||||
|
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
|
||||||
|
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
|
||||||
|
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
|
||||||
|
merged[key] = base[key].concat(mergeWith[key])
|
||||||
|
} else {
|
||||||
|
merged[key] = mergeWith[key]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged[key] = base[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var prop in mergeWith) {
|
||||||
|
if (!merged.hasOwnProperty(prop)) {
|
||||||
|
merged[prop] = mergeWith[prop]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine what "class" of input this element is given the "type".
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
classify (type) {
|
||||||
|
if (this.options.library.hasOwnProperty(type)) {
|
||||||
|
return this.options.library[type].classification
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine what type of component to render given the "type".
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
component (type) {
|
||||||
|
if (this.options.library.hasOwnProperty(type)) {
|
||||||
|
return this.options.library[type].component
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation rules by merging any passed in with global rules.
|
||||||
|
* @return {object} object of validation functions
|
||||||
|
*/
|
||||||
|
rules (rules = {}) {
|
||||||
|
return { ...this.options.rules, ...rules }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to get the vue-i18n configured locale.
|
||||||
|
*/
|
||||||
|
i18n (vm) {
|
||||||
|
if (vm.$i18n) {
|
||||||
|
switch (typeof vm.$i18n.locale) {
|
||||||
|
case 'string':
|
||||||
|
return vm.$i18n.locale
|
||||||
|
case 'function':
|
||||||
|
return vm.$i18n.locale()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation message for a particular error.
|
||||||
|
*/
|
||||||
|
validationMessage (rule, validationContext, vm) {
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an instance of a FormularioForm register it.
|
||||||
|
* @param {vm} form
|
||||||
|
*/
|
||||||
|
register (form) {
|
||||||
|
if (form.$options.name === 'FormularioForm' && form.name) {
|
||||||
|
this.registry.set(form.name, form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an instance of a form, remove it from the registry.
|
||||||
|
* @param {vm} form
|
||||||
|
*/
|
||||||
|
deregister (form) {
|
||||||
|
if (
|
||||||
|
form.$options.name === 'FormularioForm' &&
|
||||||
|
form.name &&
|
||||||
|
this.registry.has(form.name)
|
||||||
|
) {
|
||||||
|
this.registry.delete(form.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array, this function will attempt to make sense of the given error
|
||||||
|
* and hydrate a form with the resulting errors.
|
||||||
|
*
|
||||||
|
* @param {error} err
|
||||||
|
* @param {string} formName
|
||||||
|
* @param {error}
|
||||||
|
*/
|
||||||
|
handle (err, formName, skip = false) {
|
||||||
|
const e = skip ? err : this.options.errorHandler(err, formName)
|
||||||
|
if (formName && this.registry.has(formName)) {
|
||||||
|
this.registry.get(formName).applyErrors({
|
||||||
|
formErrors: arrayify(e.formErrors),
|
||||||
|
inputErrors: e.inputErrors || {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset a form.
|
||||||
|
* @param {string} formName
|
||||||
|
* @param {object} initialValue
|
||||||
|
*/
|
||||||
|
reset (formName, initialValue = {}) {
|
||||||
|
this.resetValidation(formName)
|
||||||
|
this.setValues(formName, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the form's validation messages.
|
||||||
|
* @param {string} formName
|
||||||
|
*/
|
||||||
|
resetValidation (formName) {
|
||||||
|
const form = this.registry.get(formName)
|
||||||
|
form.hideErrors(formName)
|
||||||
|
form.namedErrors = []
|
||||||
|
form.namedFieldErrors = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the form values.
|
||||||
|
* @param {string} formName
|
||||||
|
* @param {object} values
|
||||||
|
*/
|
||||||
|
setValues (formName, values) {
|
||||||
|
if (values && !Array.isArray(values) && typeof values === 'object') {
|
||||||
|
const form = this.registry.get(formName)
|
||||||
|
form.setValues({ ...values })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file uploader.
|
||||||
|
*/
|
||||||
|
getUploader () {
|
||||||
|
return this.options.uploader || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the global upload url.
|
||||||
|
*/
|
||||||
|
getUploadUrl () {
|
||||||
|
return this.options.uploadUrl || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When re-hydrating a file uploader with an array, get the sub-object key to
|
||||||
|
* access the url of the file. Usually this is just "url".
|
||||||
|
*/
|
||||||
|
getFileUrlKey () {
|
||||||
|
return this.options.fileUrlKey || 'url'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of an upload.
|
||||||
|
*/
|
||||||
|
createUpload (fileList, context) {
|
||||||
|
return new FileUpload(fileList, context, this.options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Formulario()
|
85
src/FormularioFiles.vue
Normal file
85
src/FormularioFiles.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
v-if="fileUploads.length"
|
||||||
|
class="formulario-files"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="file in fileUploads"
|
||||||
|
:key="file.uuid"
|
||||||
|
:data-has-error="!!file.error"
|
||||||
|
:data-has-preview="!!(imagePreview && file.previewData)"
|
||||||
|
>
|
||||||
|
<div class="formulario-file">
|
||||||
|
<div
|
||||||
|
v-if="!!(imagePreview && file.previewData)"
|
||||||
|
class="formulario-file-image-preview"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="file.previewData"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="formulario-file-name"
|
||||||
|
:title="file.name"
|
||||||
|
v-text="file.name"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="file.progress !== false"
|
||||||
|
:data-just-finished="file.justFinished"
|
||||||
|
:data-is-finished="!file.justFinished && file.complete"
|
||||||
|
class="formulario-file-progress"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="formulario-file-progress-inner"
|
||||||
|
:style="{width: file.progress + '%'}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="(file.complete && !file.justFinished) || file.progress === false"
|
||||||
|
class="formulario-file-remove"
|
||||||
|
@click="file.removeFile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="file.error"
|
||||||
|
class="formulario-file-upload-error"
|
||||||
|
v-text="file.error"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FileUpload from './FileUpload'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormularioFiles',
|
||||||
|
props: {
|
||||||
|
files: {
|
||||||
|
type: FileUpload,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
imagePreview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fileUploads () {
|
||||||
|
return this.files.files || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
files () {
|
||||||
|
if (this.imagePreview) {
|
||||||
|
this.files.loadPreviews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.imagePreview) {
|
||||||
|
this.files.loadPreviews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
167
src/FormularioForm.vue
Normal file
167
src/FormularioForm.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
:class="classes"
|
||||||
|
@submit.prevent="formSubmitted"
|
||||||
|
>
|
||||||
|
<slot :errors="mergedFormErrors" />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { arrayify, has } from './libs/utils'
|
||||||
|
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||||
|
import FormSubmission from './FormSubmission'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
...useRegistryProviders(this),
|
||||||
|
observeErrors: this.addErrorObserver,
|
||||||
|
removeErrorObserver: this.removeErrorObserver,
|
||||||
|
formularioFieldValidation: this.formularioFieldValidation,
|
||||||
|
path: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'FormularioForm',
|
||||||
|
model: {
|
||||||
|
prop: 'formularioValue',
|
||||||
|
event: 'input'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
formularioValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
formErrors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ([])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
...useRegistry(this),
|
||||||
|
formShouldShowErrors: false,
|
||||||
|
errorObservers: [],
|
||||||
|
namedErrors: [],
|
||||||
|
namedFieldErrors: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...useRegistryComputed(),
|
||||||
|
classes () {
|
||||||
|
const classes = { 'formulario-form': true }
|
||||||
|
if (this.name) {
|
||||||
|
classes[`formulario-form--${this.name}`] = true
|
||||||
|
}
|
||||||
|
return classes
|
||||||
|
},
|
||||||
|
mergedFormErrors () {
|
||||||
|
return this.formErrors.concat(this.namedErrors)
|
||||||
|
},
|
||||||
|
mergedFieldErrors () {
|
||||||
|
const errors = {}
|
||||||
|
if (this.errors) {
|
||||||
|
for (const fieldName in this.errors) {
|
||||||
|
errors[fieldName] = arrayify(this.errors[fieldName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fieldName in this.namedFieldErrors) {
|
||||||
|
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
},
|
||||||
|
hasFormErrorObservers () {
|
||||||
|
return !!this.errorObservers.filter(o => o.type === 'form').length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
formularioValue: {
|
||||||
|
handler (values) {
|
||||||
|
if (this.isVmodeled &&
|
||||||
|
values &&
|
||||||
|
typeof values === 'object'
|
||||||
|
) {
|
||||||
|
this.setValues(values)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
mergedFormErrors (errors) {
|
||||||
|
this.errorObservers
|
||||||
|
.filter(o => o.type === 'form')
|
||||||
|
.forEach(o => o.callback(errors))
|
||||||
|
},
|
||||||
|
mergedFieldErrors: {
|
||||||
|
handler (errors) {
|
||||||
|
this.errorObservers
|
||||||
|
.filter(o => o.type === 'input')
|
||||||
|
.forEach(o => o.callback(errors[o.field] || []))
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$formulario.register(this)
|
||||||
|
this.applyInitialValues()
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$formulario.deregister(this)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...useRegistryMethods(),
|
||||||
|
applyErrors ({ formErrors, inputErrors }) {
|
||||||
|
// given an object of errors, apply them to this form
|
||||||
|
this.namedErrors = formErrors
|
||||||
|
this.namedFieldErrors = inputErrors
|
||||||
|
},
|
||||||
|
addErrorObserver (observer) {
|
||||||
|
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
|
||||||
|
this.errorObservers.push(observer)
|
||||||
|
if (observer.type === 'form') {
|
||||||
|
observer.callback(this.mergedFormErrors)
|
||||||
|
} else if (has(this.mergedFieldErrors, observer.field)) {
|
||||||
|
observer.callback(this.mergedFieldErrors[observer.field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeErrorObserver (observer) {
|
||||||
|
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
||||||
|
},
|
||||||
|
registerErrorComponent (component) {
|
||||||
|
if (!this.errorComponents.includes(component)) {
|
||||||
|
this.errorComponents.push(component)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formSubmitted () {
|
||||||
|
// perform validation here
|
||||||
|
this.showErrors()
|
||||||
|
const submission = new FormSubmission(this)
|
||||||
|
this.$emit('submit-raw', submission)
|
||||||
|
return submission.hasValidationErrors()
|
||||||
|
.then(hasErrors => hasErrors ? undefined : submission.values())
|
||||||
|
.then(data => {
|
||||||
|
if (typeof data !== 'undefined') {
|
||||||
|
this.$emit('submit', data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formularioFieldValidation (errorObject) {
|
||||||
|
this.$emit('validation', errorObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
43
src/FormularioGrouping.vue
Normal file
43
src/FormularioGrouping.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="formulario-group"
|
||||||
|
data-type="group"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FormularioGrouping',
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isArrayItem: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
path: this.groupPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: ['path'],
|
||||||
|
computed: {
|
||||||
|
groupPath () {
|
||||||
|
if (this.isArrayItem) {
|
||||||
|
return this.path + '[' + this.name + ']';
|
||||||
|
} else {
|
||||||
|
if (this.path === '') {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.path + '.' + this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
331
src/FormularioInput.vue
Normal file
331
src/FormularioInput.vue
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="formulario-input"
|
||||||
|
:data-has-errors="hasErrors"
|
||||||
|
:data-is-showing-errors="hasVisibleErrors"
|
||||||
|
:data-type="type"
|
||||||
|
>
|
||||||
|
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import context from './libs/context'
|
||||||
|
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormularioInput',
|
||||||
|
inheritAttrs: false,
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
// Allows sub-components of this input to register arbitrary rules.
|
||||||
|
formularioRegisterRule: this.registerRule,
|
||||||
|
formularioRemoveRule: this.removeRule
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
formularioSetter: { default: undefined },
|
||||||
|
formularioFieldValidation: { default: () => () => ({}) },
|
||||||
|
formularioRegister: { default: undefined },
|
||||||
|
formularioDeregister: { default: undefined },
|
||||||
|
getFormValues: { default: () => () => ({}) },
|
||||||
|
observeErrors: { default: undefined },
|
||||||
|
removeErrorObserver: { default: undefined },
|
||||||
|
path: { default: '' }
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'formularioValue',
|
||||||
|
event: 'input'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
/* eslint-disable */
|
||||||
|
formularioValue: {
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/* eslint-enable */
|
||||||
|
id: {
|
||||||
|
type: [String, Boolean, Number],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
type: [String, Array, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
type: [String, Boolean, Array],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
validationName: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
errorBehavior: {
|
||||||
|
type: String,
|
||||||
|
default: 'blur',
|
||||||
|
validator: function (value) {
|
||||||
|
return ['blur', 'live', 'submit'].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showErrors: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
imageBehavior: {
|
||||||
|
type: String,
|
||||||
|
default: 'preview'
|
||||||
|
},
|
||||||
|
uploadUrl: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
uploader: {
|
||||||
|
type: [Function, Object, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
uploadBehavior: {
|
||||||
|
type: String,
|
||||||
|
default: 'live'
|
||||||
|
},
|
||||||
|
preventWindowDrops: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
validationMessages: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
validationRules: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
disableErrors: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
defaultId: this.$formulario.nextId(this),
|
||||||
|
localAttributes: {},
|
||||||
|
localErrors: [],
|
||||||
|
proxy: this.getInitialValue(),
|
||||||
|
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
||||||
|
formShouldShowErrors: false,
|
||||||
|
validationErrors: [],
|
||||||
|
pendingValidation: Promise.resolve(),
|
||||||
|
// These registries are used for injected messages registrants only (mostly internal).
|
||||||
|
ruleRegistry: [],
|
||||||
|
messageRegistry: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...context,
|
||||||
|
parsedValidationRules () {
|
||||||
|
const parsedValidationRules = {}
|
||||||
|
Object.keys(this.validationRules).forEach(key => {
|
||||||
|
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
||||||
|
})
|
||||||
|
return parsedValidationRules
|
||||||
|
},
|
||||||
|
messages () {
|
||||||
|
const messages = {}
|
||||||
|
Object.keys(this.validationMessages).forEach((key) => {
|
||||||
|
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||||
|
})
|
||||||
|
Object.keys(this.messageRegistry).forEach((key) => {
|
||||||
|
messages[snakeToCamel(key)] = this.messageRegistry[key]
|
||||||
|
})
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$attrs': {
|
||||||
|
handler (value) {
|
||||||
|
this.updateLocalAttributes(value)
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
proxy (newValue, oldValue) {
|
||||||
|
this.performValidation()
|
||||||
|
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
|
this.context.model = newValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formularioValue (newValue, oldValue) {
|
||||||
|
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
|
this.context.model = newValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showValidationErrors: {
|
||||||
|
handler (val) {
|
||||||
|
this.$emit('error-visibility', val)
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.applyInitialValue()
|
||||||
|
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
||||||
|
this.formularioRegister(this.nameOrFallback, this)
|
||||||
|
}
|
||||||
|
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
||||||
|
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||||
|
}
|
||||||
|
this.updateLocalAttributes(this.$attrs)
|
||||||
|
this.performValidation()
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
||||||
|
this.removeErrorObserver(this.setErrors)
|
||||||
|
}
|
||||||
|
if (typeof this.formularioDeregister === 'function') {
|
||||||
|
this.formularioDeregister(this.nameOrFallback)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getInitialValue () {
|
||||||
|
if (has(this.$options.propsData, 'value')) {
|
||||||
|
return this.value
|
||||||
|
} else if (has(this.$options.propsData, 'formularioValue')) {
|
||||||
|
return this.formularioValue
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
applyInitialValue () {
|
||||||
|
// This should only be run immediately on created and ensures that the
|
||||||
|
// proxy and the model are both the same before any additional registration.
|
||||||
|
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
||||||
|
this.context.model = this.proxy
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateLocalAttributes (value) {
|
||||||
|
if (!shallowEqualObjects(value, this.localAttributes)) {
|
||||||
|
this.localAttributes = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
performValidation () {
|
||||||
|
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
||||||
|
// Add in ruleRegistry rules. These are added directly via injection from
|
||||||
|
// children and not part of the standard validation rule set.
|
||||||
|
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
||||||
|
this.pendingValidation = this.runRules(rules)
|
||||||
|
.then(messages => this.didValidate(messages))
|
||||||
|
return this.pendingValidation
|
||||||
|
},
|
||||||
|
runRules (rules) {
|
||||||
|
const run = ([rule, args, ruleName, modifier]) => {
|
||||||
|
var res = rule({
|
||||||
|
value: this.context.model,
|
||||||
|
getFormValues: this.getFormValues.bind(this),
|
||||||
|
name: this.context.name
|
||||||
|
}, ...args)
|
||||||
|
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
||||||
|
return res.then(result => result ? false : this.getMessage(ruleName, args))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const resolveGroups = (groups, allMessages = []) => {
|
||||||
|
const ruleGroup = groups.shift()
|
||||||
|
if (Array.isArray(ruleGroup) && ruleGroup.length) {
|
||||||
|
Promise.all(ruleGroup.map(run))
|
||||||
|
.then(messages => messages.filter(m => !!m))
|
||||||
|
.then(messages => {
|
||||||
|
messages = Array.isArray(messages) ? messages : []
|
||||||
|
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
||||||
|
if ((!messages.length || !ruleGroup.bail) && groups.length) {
|
||||||
|
return resolveGroups(groups, allMessages.concat(messages))
|
||||||
|
}
|
||||||
|
return resolve(allMessages.concat(messages))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolveGroups(groupBails(rules))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
didValidate (messages) {
|
||||||
|
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
||||||
|
this.validationErrors = messages
|
||||||
|
if (validationChanged) {
|
||||||
|
const errorObject = this.getErrorObject()
|
||||||
|
this.$emit('validation', errorObject)
|
||||||
|
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
|
||||||
|
this.formularioFieldValidation(errorObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getMessage (ruleName, args) {
|
||||||
|
return this.getMessageFunc(ruleName)({
|
||||||
|
args,
|
||||||
|
name: this.mergedValidationName,
|
||||||
|
value: this.context.model,
|
||||||
|
vm: this,
|
||||||
|
formValues: this.getFormValues()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMessageFunc (ruleName) {
|
||||||
|
ruleName = snakeToCamel(ruleName)
|
||||||
|
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
||||||
|
switch (typeof this.messages[ruleName]) {
|
||||||
|
case 'function':
|
||||||
|
return this.messages[ruleName]
|
||||||
|
case 'string':
|
||||||
|
case 'boolean':
|
||||||
|
return () => this.messages[ruleName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (context) => this.$formulario.validationMessage(ruleName, context, this)
|
||||||
|
},
|
||||||
|
hasValidationErrors () {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getValidationErrors () {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getErrorObject () {
|
||||||
|
return {
|
||||||
|
name: this.context.nameOrFallback || this.context.name,
|
||||||
|
errors: this.validationErrors.filter(s => typeof s === 'string'),
|
||||||
|
hasErrors: !!this.validationErrors.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setErrors (errors) {
|
||||||
|
this.localErrors = arrayify(errors)
|
||||||
|
},
|
||||||
|
registerRule (rule, args, ruleName, message = null) {
|
||||||
|
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
|
||||||
|
// These are the raw rule format since they will be used directly.
|
||||||
|
this.ruleRegistry.push([rule, args, ruleName])
|
||||||
|
if (message !== null) {
|
||||||
|
this.messageRegistry[ruleName] = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeRule (key) {
|
||||||
|
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
||||||
|
if (ruleIndex >= 0) {
|
||||||
|
this.ruleRegistry.splice(ruleIndex, 1)
|
||||||
|
delete this.messageRegistry[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
352
src/Formulate.js
352
src/Formulate.js
@ -1,352 +0,0 @@
|
|||||||
import library from './libs/library'
|
|
||||||
import rules from './libs/rules'
|
|
||||||
import mimes from './libs/mimes'
|
|
||||||
import FileUpload from './FileUpload'
|
|
||||||
import { arrayify, parseLocale, has } from './libs/utils'
|
|
||||||
import isPlainObject from 'is-plain-object'
|
|
||||||
import { en } from '@braid/vue-formulate-i18n'
|
|
||||||
import fauxUploader from './libs/faux-uploader'
|
|
||||||
import FormulateSlot from './FormulateSlot'
|
|
||||||
import FormulateForm from './FormulateForm.vue'
|
|
||||||
import FormulateInput from './FormulateInput.vue'
|
|
||||||
import FormulateErrors from './FormulateErrors.vue'
|
|
||||||
import FormulateHelp from './slots/FormulateHelp.vue'
|
|
||||||
import FormulateGrouping from './FormulateGrouping.vue'
|
|
||||||
import FormulateLabel from './slots/FormulateLabel.vue'
|
|
||||||
import FormulateAddMore from './slots/FormulateAddMore.vue'
|
|
||||||
import FormulateRepeatable from './slots/FormulateRepeatable.vue'
|
|
||||||
import FormulateInputGroup from './inputs/FormulateInputGroup.vue'
|
|
||||||
import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue'
|
|
||||||
import FormulateRepeatableRemove from './slots/FormulateRepeatableRemove.vue'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base formulate library.
|
|
||||||
*/
|
|
||||||
class Formulate {
|
|
||||||
/**
|
|
||||||
* Instantiate our base options.
|
|
||||||
*/
|
|
||||||
constructor () {
|
|
||||||
this.options = {}
|
|
||||||
this.defaults = {
|
|
||||||
components: {
|
|
||||||
FormulateSlot,
|
|
||||||
FormulateForm,
|
|
||||||
FormulateHelp,
|
|
||||||
FormulateLabel,
|
|
||||||
FormulateInput,
|
|
||||||
FormulateErrors,
|
|
||||||
FormulateAddMore,
|
|
||||||
FormulateGrouping,
|
|
||||||
FormulateRepeatable,
|
|
||||||
FormulateInputGroup,
|
|
||||||
FormulateRepeatableRemove,
|
|
||||||
FormulateRepeatableProvider
|
|
||||||
},
|
|
||||||
slotComponents: {
|
|
||||||
label: 'FormulateLabel',
|
|
||||||
help: 'FormulateHelp',
|
|
||||||
errors: 'FormulateErrors',
|
|
||||||
repeatable: 'FormulateRepeatable',
|
|
||||||
addMore: 'FormulateAddMore',
|
|
||||||
remove: 'FormulateRepeatableRemove'
|
|
||||||
},
|
|
||||||
library,
|
|
||||||
rules,
|
|
||||||
mimes,
|
|
||||||
locale: false,
|
|
||||||
uploader: fauxUploader,
|
|
||||||
uploadUrl: false,
|
|
||||||
fileUrlKey: 'url',
|
|
||||||
uploadJustCompleteDuration: 1000,
|
|
||||||
errorHandler: (err) => err,
|
|
||||||
plugins: [ en ],
|
|
||||||
locales: {},
|
|
||||||
idPrefix: 'formulate-'
|
|
||||||
}
|
|
||||||
this.registry = new Map()
|
|
||||||
this.idRegistry = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install vue formulate, and register it’s components.
|
|
||||||
*/
|
|
||||||
install (Vue, options) {
|
|
||||||
Vue.prototype.$formulate = this
|
|
||||||
this.options = this.defaults
|
|
||||||
var plugins = this.defaults.plugins
|
|
||||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
|
||||||
plugins = plugins.concat(options.plugins)
|
|
||||||
}
|
|
||||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
|
||||||
this.extend(options || {})
|
|
||||||
for (var componentName in this.options.components) {
|
|
||||||
Vue.component(componentName, this.options.components[componentName])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Produce a deterministically generated id based on the sequence by which it
|
|
||||||
* was requested. This should be *theoretically* the same SSR as client side.
|
|
||||||
* However, SSR and deterministic ids can be very challenging, so this
|
|
||||||
* implementation is open to community review.
|
|
||||||
*/
|
|
||||||
nextId (vm) {
|
|
||||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
|
||||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
|
|
||||||
this.idRegistry[pathPrefix] = 0
|
|
||||||
}
|
|
||||||
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a set of options, apply them to the pre-existing options.
|
|
||||||
* @param {Object} extendWith
|
|
||||||
*/
|
|
||||||
extend (extendWith) {
|
|
||||||
if (typeof extendWith === 'object') {
|
|
||||||
this.options = this.merge(this.options, extendWith)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
throw new Error(`VueFormulate extend() should be passed an object (was ${typeof extendWith})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new object by copying properties of base and mergeWith.
|
|
||||||
* Note: arrays don't overwrite - they push
|
|
||||||
*
|
|
||||||
* @param {Object} base
|
|
||||||
* @param {Object} mergeWith
|
|
||||||
* @param {boolean} concatArrays
|
|
||||||
*/
|
|
||||||
merge (base, mergeWith, concatArrays = true) {
|
|
||||||
var merged = {}
|
|
||||||
for (var key in base) {
|
|
||||||
if (mergeWith.hasOwnProperty(key)) {
|
|
||||||
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
|
|
||||||
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
|
|
||||||
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
|
|
||||||
merged[key] = base[key].concat(mergeWith[key])
|
|
||||||
} else {
|
|
||||||
merged[key] = mergeWith[key]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
merged[key] = base[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var prop in mergeWith) {
|
|
||||||
if (!merged.hasOwnProperty(prop)) {
|
|
||||||
merged[prop] = mergeWith[prop]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine what "class" of input this element is given the "type".
|
|
||||||
* @param {string} type
|
|
||||||
*/
|
|
||||||
classify (type) {
|
|
||||||
if (this.options.library.hasOwnProperty(type)) {
|
|
||||||
return this.options.library[type].classification
|
|
||||||
}
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine what type of component to render given the "type".
|
|
||||||
* @param {string} type
|
|
||||||
*/
|
|
||||||
component (type) {
|
|
||||||
if (this.options.library.hasOwnProperty(type)) {
|
|
||||||
return this.options.library[type].component
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What component should be rendered for the given slot location and type.
|
|
||||||
* @param {string} type the type of component
|
|
||||||
* @param {string} slot the name of the slot
|
|
||||||
*/
|
|
||||||
slotComponent (type, slot) {
|
|
||||||
const def = this.options.library[type]
|
|
||||||
if (def && def.slotComponents && def.slotComponents[slot]) {
|
|
||||||
return def.slotComponents[slot]
|
|
||||||
}
|
|
||||||
return this.options.slotComponents[slot]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get validation rules by merging any passed in with global rules.
|
|
||||||
* @return {object} object of validation functions
|
|
||||||
*/
|
|
||||||
rules (rules = {}) {
|
|
||||||
return { ...this.options.rules, ...rules }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to get the vue-i18n configured locale.
|
|
||||||
*/
|
|
||||||
i18n (vm) {
|
|
||||||
if (vm.$i18n) {
|
|
||||||
switch (typeof vm.$i18n.locale) {
|
|
||||||
case 'string':
|
|
||||||
return vm.$i18n.locale
|
|
||||||
case 'function':
|
|
||||||
return vm.$i18n.locale()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the proper locale to use.
|
|
||||||
*/
|
|
||||||
getLocale (vm) {
|
|
||||||
if (!this.selectedLocale) {
|
|
||||||
this.selectedLocale = [
|
|
||||||
this.options.locale,
|
|
||||||
this.i18n(vm),
|
|
||||||
'en'
|
|
||||||
].reduce((selection, locale) => {
|
|
||||||
if (selection) {
|
|
||||||
return selection
|
|
||||||
}
|
|
||||||
if (locale) {
|
|
||||||
const option = parseLocale(locale)
|
|
||||||
.find(locale => has(this.options.locales, locale))
|
|
||||||
if (option) {
|
|
||||||
selection = option
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selection
|
|
||||||
}, false)
|
|
||||||
}
|
|
||||||
return this.selectedLocale
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation message for a particular error.
|
|
||||||
*/
|
|
||||||
validationMessage (rule, validationContext, vm) {
|
|
||||||
const generators = this.options.locales[this.getLocale(vm)]
|
|
||||||
if (generators.hasOwnProperty(rule)) {
|
|
||||||
return generators[rule](validationContext)
|
|
||||||
} else if (rule[0] === '_' && generators.hasOwnProperty(rule.substr(1))) {
|
|
||||||
return generators[rule.substr(1)](validationContext)
|
|
||||||
}
|
|
||||||
if (generators.hasOwnProperty('default')) {
|
|
||||||
return generators.default(validationContext)
|
|
||||||
}
|
|
||||||
return 'This field does not have a valid value'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an instance of a FormulateForm register it.
|
|
||||||
* @param {vm} form
|
|
||||||
*/
|
|
||||||
register (form) {
|
|
||||||
if (form.$options.name === 'FormulateForm' && form.name) {
|
|
||||||
this.registry.set(form.name, form)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an instance of a form, remove it from the registry.
|
|
||||||
* @param {vm} form
|
|
||||||
*/
|
|
||||||
deregister (form) {
|
|
||||||
if (
|
|
||||||
form.$options.name === 'FormulateForm' &&
|
|
||||||
form.name &&
|
|
||||||
this.registry.has(form.name)
|
|
||||||
) {
|
|
||||||
this.registry.delete(form.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an array, this function will attempt to make sense of the given error
|
|
||||||
* and hydrate a form with the resulting errors.
|
|
||||||
*
|
|
||||||
* @param {error} err
|
|
||||||
* @param {string} formName
|
|
||||||
* @param {error}
|
|
||||||
*/
|
|
||||||
handle (err, formName, skip = false) {
|
|
||||||
const e = skip ? err : this.options.errorHandler(err, formName)
|
|
||||||
if (formName && this.registry.has(formName)) {
|
|
||||||
this.registry.get(formName).applyErrors({
|
|
||||||
formErrors: arrayify(e.formErrors),
|
|
||||||
inputErrors: e.inputErrors || {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset a form.
|
|
||||||
* @param {string} formName
|
|
||||||
* @param {object} initialValue
|
|
||||||
*/
|
|
||||||
reset (formName, initialValue = {}) {
|
|
||||||
this.resetValidation(formName)
|
|
||||||
this.setValues(formName, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the form's validation messages.
|
|
||||||
* @param {string} formName
|
|
||||||
*/
|
|
||||||
resetValidation (formName) {
|
|
||||||
const form = this.registry.get(formName)
|
|
||||||
form.hideErrors(formName)
|
|
||||||
form.namedErrors = []
|
|
||||||
form.namedFieldErrors = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the form values.
|
|
||||||
* @param {string} formName
|
|
||||||
* @param {object} values
|
|
||||||
*/
|
|
||||||
setValues (formName, values) {
|
|
||||||
if (values && !Array.isArray(values) && typeof values === 'object') {
|
|
||||||
const form = this.registry.get(formName)
|
|
||||||
form.setValues({ ...values })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file uploader.
|
|
||||||
*/
|
|
||||||
getUploader () {
|
|
||||||
return this.options.uploader || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the global upload url.
|
|
||||||
*/
|
|
||||||
getUploadUrl () {
|
|
||||||
return this.options.uploadUrl || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When re-hydrating a file uploader with an array, get the sub-object key to
|
|
||||||
* access the url of the file. Usually this is just "url".
|
|
||||||
*/
|
|
||||||
getFileUrlKey () {
|
|
||||||
return this.options.fileUrlKey || 'url'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance of an upload.
|
|
||||||
*/
|
|
||||||
createUpload (fileList, context) {
|
|
||||||
return new FileUpload(fileList, context, this.options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Formulate()
|
|
@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ul
|
|
||||||
v-if="visibleErrors.length"
|
|
||||||
:class="`formulate-${type}-errors`"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="error in visibleErrors"
|
|
||||||
:key="error"
|
|
||||||
:class="`formulate-${type}-error`"
|
|
||||||
v-text="error"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { arrayify } from './libs/utils'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
inject: {
|
|
||||||
observeErrors: {
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
removeErrorObserver: {
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'form'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
boundSetErrors: this.setErrors.bind(this),
|
|
||||||
localErrors: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
visibleValidationErrors () {
|
|
||||||
return Array.isArray(this.context.visibleValidationErrors) ? this.context.visibleValidationErrors : []
|
|
||||||
},
|
|
||||||
errors () {
|
|
||||||
return Array.isArray(this.context.errors) ? this.context.errors : []
|
|
||||||
},
|
|
||||||
mergedErrors () {
|
|
||||||
return this.errors.concat(this.localErrors)
|
|
||||||
},
|
|
||||||
visibleErrors () {
|
|
||||||
return Array.from(new Set(this.mergedErrors.concat(this.visibleValidationErrors)))
|
|
||||||
.filter(message => typeof message === 'string')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
// This registration is for <FormulateErrors /> that are used for displaying
|
|
||||||
// Form errors in an override position.
|
|
||||||
if (this.type === 'form' && typeof this.observeErrors === 'function' && !Array.isArray(this.context.errors)) {
|
|
||||||
this.observeErrors({ callback: this.boundSetErrors, type: this.type })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
if (this.type === 'form' && typeof this.removeErrorObserver === 'function' && !Array.isArray(this.context.errors)) {
|
|
||||||
this.removeErrorObserver(this.boundSetErrors)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setErrors (errors) {
|
|
||||||
this.localErrors = arrayify(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,85 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ul
|
|
||||||
v-if="fileUploads.length"
|
|
||||||
class="formulate-files"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="file in fileUploads"
|
|
||||||
:key="file.uuid"
|
|
||||||
:data-has-error="!!file.error"
|
|
||||||
:data-has-preview="!!(imagePreview && file.previewData)"
|
|
||||||
>
|
|
||||||
<div class="formulate-file">
|
|
||||||
<div
|
|
||||||
v-if="!!(imagePreview && file.previewData)"
|
|
||||||
class="formulate-file-image-preview"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="file.previewData"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="formulate-file-name"
|
|
||||||
:title="file.name"
|
|
||||||
v-text="file.name"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="file.progress !== false"
|
|
||||||
:data-just-finished="file.justFinished"
|
|
||||||
:data-is-finished="!file.justFinished && file.complete"
|
|
||||||
class="formulate-file-progress"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="formulate-file-progress-inner"
|
|
||||||
:style="{width: file.progress + '%'}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="(file.complete && !file.justFinished) || file.progress === false"
|
|
||||||
class="formulate-file-remove"
|
|
||||||
@click="file.removeFile"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="file.error"
|
|
||||||
class="formulate-file-upload-error"
|
|
||||||
v-text="file.error"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import FileUpload from './FileUpload'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FormulateFiles',
|
|
||||||
props: {
|
|
||||||
files: {
|
|
||||||
type: FileUpload,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
imagePreview: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fileUploads () {
|
|
||||||
return this.files.files || []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
files () {
|
|
||||||
if (this.imagePreview) {
|
|
||||||
this.files.loadPreviews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
if (this.imagePreview) {
|
|
||||||
this.files.loadPreviews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,175 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form
|
|
||||||
:class="classes"
|
|
||||||
@submit.prevent="formSubmitted"
|
|
||||||
>
|
|
||||||
<FormulateErrors
|
|
||||||
v-if="!hasFormErrorObservers"
|
|
||||||
:context="formContext"
|
|
||||||
/>
|
|
||||||
<slot />
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { arrayify, has } from './libs/utils'
|
|
||||||
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
|
||||||
import FormSubmission from './FormSubmission'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
provide () {
|
|
||||||
return {
|
|
||||||
...useRegistryProviders(this),
|
|
||||||
observeErrors: this.addErrorObserver,
|
|
||||||
removeErrorObserver: this.removeErrorObserver,
|
|
||||||
formulateFieldValidation: this.formulateFieldValidation
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: 'FormulateForm',
|
|
||||||
model: {
|
|
||||||
prop: 'formulateValue',
|
|
||||||
event: 'input'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
formulateValue: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
type: [Object, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
type: [Object, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
formErrors: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ([])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
...useRegistry(this),
|
|
||||||
formShouldShowErrors: false,
|
|
||||||
errorObservers: [],
|
|
||||||
namedErrors: [],
|
|
||||||
namedFieldErrors: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...useRegistryComputed(),
|
|
||||||
formContext () {
|
|
||||||
return {
|
|
||||||
errors: this.mergedFormErrors
|
|
||||||
}
|
|
||||||
},
|
|
||||||
classes () {
|
|
||||||
const classes = { 'formulate-form': true }
|
|
||||||
if (this.name) {
|
|
||||||
classes[`formulate-form--${this.name}`] = true
|
|
||||||
}
|
|
||||||
return classes
|
|
||||||
},
|
|
||||||
mergedFormErrors () {
|
|
||||||
return this.formErrors.concat(this.namedErrors)
|
|
||||||
},
|
|
||||||
mergedFieldErrors () {
|
|
||||||
const errors = {}
|
|
||||||
if (this.errors) {
|
|
||||||
for (const fieldName in this.errors) {
|
|
||||||
errors[fieldName] = arrayify(this.errors[fieldName])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const fieldName in this.namedFieldErrors) {
|
|
||||||
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
},
|
|
||||||
hasFormErrorObservers () {
|
|
||||||
return !!this.errorObservers.filter(o => o.type === 'form').length
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
formulateValue: {
|
|
||||||
handler (values) {
|
|
||||||
if (this.isVmodeled &&
|
|
||||||
values &&
|
|
||||||
typeof values === 'object'
|
|
||||||
) {
|
|
||||||
this.setValues(values)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
mergedFormErrors (errors) {
|
|
||||||
this.errorObservers
|
|
||||||
.filter(o => o.type === 'form')
|
|
||||||
.forEach(o => o.callback(errors))
|
|
||||||
},
|
|
||||||
mergedFieldErrors: {
|
|
||||||
handler (errors) {
|
|
||||||
this.errorObservers
|
|
||||||
.filter(o => o.type === 'input')
|
|
||||||
.forEach(o => o.callback(errors[o.field] || []))
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.$formulate.register(this)
|
|
||||||
this.applyInitialValues()
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
this.$formulate.deregister(this)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...useRegistryMethods(),
|
|
||||||
applyErrors ({ formErrors, inputErrors }) {
|
|
||||||
// given an object of errors, apply them to this form
|
|
||||||
this.namedErrors = formErrors
|
|
||||||
this.namedFieldErrors = inputErrors
|
|
||||||
},
|
|
||||||
addErrorObserver (observer) {
|
|
||||||
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
|
|
||||||
this.errorObservers.push(observer)
|
|
||||||
if (observer.type === 'form') {
|
|
||||||
observer.callback(this.mergedFormErrors)
|
|
||||||
} else if (has(this.mergedFieldErrors, observer.field)) {
|
|
||||||
observer.callback(this.mergedFieldErrors[observer.field])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeErrorObserver (observer) {
|
|
||||||
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
|
||||||
},
|
|
||||||
registerErrorComponent (component) {
|
|
||||||
if (!this.errorComponents.includes(component)) {
|
|
||||||
this.errorComponents.push(component)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formSubmitted () {
|
|
||||||
// perform validation here
|
|
||||||
this.showErrors()
|
|
||||||
const submission = new FormSubmission(this)
|
|
||||||
this.$emit('submit-raw', submission)
|
|
||||||
return submission.hasValidationErrors()
|
|
||||||
.then(hasErrors => hasErrors ? undefined : submission.values())
|
|
||||||
.then(data => {
|
|
||||||
if (typeof data !== 'undefined') {
|
|
||||||
this.$emit('submit', data)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
},
|
|
||||||
formulateFieldValidation (errorObject) {
|
|
||||||
this.$emit('validation', errorObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<FormulateSlot
|
|
||||||
name="grouping"
|
|
||||||
class="formulate-input-grouping"
|
|
||||||
:context="context"
|
|
||||||
:force-wrap="context.repeatable"
|
|
||||||
>
|
|
||||||
<FormulateRepeatableProvider
|
|
||||||
v-for="(item, index) in items"
|
|
||||||
:key="item.__id"
|
|
||||||
:index="index"
|
|
||||||
:set-field-value="(field, value) => setFieldValue(index, field, value)"
|
|
||||||
:context="context"
|
|
||||||
@remove="removeItem"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</FormulateRepeatableProvider>
|
|
||||||
</FormulateSlot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { setId } from './libs/utils'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FormulateGrouping',
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide () {
|
|
||||||
return {
|
|
||||||
isSubField: () => true,
|
|
||||||
registerProvider: this.registerProvider,
|
|
||||||
deregisterProvider: this.deregisterProvider
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
providers: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inject: ['formulateRegisterRule', 'formulateRemoveRule'],
|
|
||||||
computed: {
|
|
||||||
items () {
|
|
||||||
if (Array.isArray(this.context.model)) {
|
|
||||||
if (!this.context.repeatable && this.context.model.length === 0) {
|
|
||||||
return [setId({})]
|
|
||||||
}
|
|
||||||
return this.context.model.map(item => setId(item, item.__id))
|
|
||||||
}
|
|
||||||
return [setId({})]
|
|
||||||
},
|
|
||||||
formShouldShowErrors () {
|
|
||||||
return this.context.formShouldShowErrors
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
providers () {
|
|
||||||
if (this.formShouldShowErrors) {
|
|
||||||
this.showErrors()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formShouldShowErrors (val) {
|
|
||||||
if (val) {
|
|
||||||
this.showErrors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
// We register with an error message of 'true' which causes the validation to fail but no message output.
|
|
||||||
this.formulateRegisterRule(this.validateGroup.bind(this), [], 'formulateGrouping', true)
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
this.formulateRemoveRule('formulateGrouping')
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getAtIndex (index) {
|
|
||||||
if (typeof this.context.model[index] !== 'undefined' && this.context.model[index].__id) {
|
|
||||||
return this.context.model[index]
|
|
||||||
} else if (typeof this.context.model[index] !== 'undefined') {
|
|
||||||
return setId(this.context.model[index])
|
|
||||||
} else if (typeof this.context.model[index] === 'undefined' && typeof this.items[index] !== 'undefined') {
|
|
||||||
return setId({}, this.items[index].__id)
|
|
||||||
}
|
|
||||||
return setId({})
|
|
||||||
},
|
|
||||||
setFieldValue (index, field, value) {
|
|
||||||
const values = Array.isArray(this.context.model) ? this.context.model : []
|
|
||||||
const previous = this.getAtIndex(index)
|
|
||||||
const updated = setId(Object.assign({}, previous, { [field]: value }), previous.__id)
|
|
||||||
values.splice(index, 1, updated)
|
|
||||||
this.context.model = values
|
|
||||||
},
|
|
||||||
validateGroup () {
|
|
||||||
return Promise.all(this.providers.reduce((resolvers, provider) => {
|
|
||||||
if (provider && typeof provider.hasValidationErrors === 'function') {
|
|
||||||
resolvers.push(provider.hasValidationErrors())
|
|
||||||
}
|
|
||||||
return resolvers
|
|
||||||
}, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors))
|
|
||||||
},
|
|
||||||
showErrors () {
|
|
||||||
this.providers.forEach(p => p && typeof p.showErrors === 'function' && p.showErrors())
|
|
||||||
},
|
|
||||||
removeItem (index) {
|
|
||||||
if (Array.isArray(this.context.model)) {
|
|
||||||
this.context.model.splice(index, 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
registerProvider (provider) {
|
|
||||||
if (!this.providers.some(p => p === provider)) {
|
|
||||||
this.providers.push(provider)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deregisterProvider (provider) {
|
|
||||||
this.providers = this.providers.filter(p => p !== provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,396 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="formulate-input"
|
|
||||||
:data-classification="classification"
|
|
||||||
:data-has-errors="hasErrors"
|
|
||||||
:data-is-showing-errors="hasVisibleErrors"
|
|
||||||
:data-type="type"
|
|
||||||
>
|
|
||||||
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import context from './libs/context'
|
|
||||||
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FormulateInput',
|
|
||||||
inheritAttrs: false,
|
|
||||||
provide () {
|
|
||||||
return {
|
|
||||||
// Allows sub-components of this input to register arbitrary rules.
|
|
||||||
formulateRegisterRule: this.registerRule,
|
|
||||||
formulateRemoveRule: this.removeRule
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inject: {
|
|
||||||
formulateSetter: { default: undefined },
|
|
||||||
formulateFieldValidation: { default: () => () => ({}) },
|
|
||||||
formulateRegister: { default: undefined },
|
|
||||||
formulateDeregister: { default: undefined },
|
|
||||||
getFormValues: { default: () => () => ({}) },
|
|
||||||
observeErrors: { default: undefined },
|
|
||||||
removeErrorObserver: { default: undefined },
|
|
||||||
isSubField: { default: () => () => false }
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
prop: 'formulateValue',
|
|
||||||
event: 'input'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'text'
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
/* eslint-disable */
|
|
||||||
formulateValue: {
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
/* eslint-enable */
|
|
||||||
options: {
|
|
||||||
type: [Object, Array, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
optionGroups: {
|
|
||||||
type: [Object, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: [String, Boolean, Number],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
labelPosition: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
limit: {
|
|
||||||
type: Number,
|
|
||||||
default: Infinity
|
|
||||||
},
|
|
||||||
help: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
helpPosition: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
type: [String, Array, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
repeatable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
type: [String, Boolean, Array],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
validationName: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
errorBehavior: {
|
|
||||||
type: String,
|
|
||||||
default: 'blur',
|
|
||||||
validator: function (value) {
|
|
||||||
return ['blur', 'live', 'submit'].includes(value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showErrors: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
imageBehavior: {
|
|
||||||
type: String,
|
|
||||||
default: 'preview'
|
|
||||||
},
|
|
||||||
uploadUrl: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
uploader: {
|
|
||||||
type: [Function, Object, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
uploadBehavior: {
|
|
||||||
type: String,
|
|
||||||
default: 'live'
|
|
||||||
},
|
|
||||||
preventWindowDrops: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showValue: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
validationMessages: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
validationRules: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
checked: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
disableErrors: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
addLabel: {
|
|
||||||
type: [Boolean, String],
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
defaultId: this.$formulate.nextId(this),
|
|
||||||
localAttributes: {},
|
|
||||||
localErrors: [],
|
|
||||||
proxy: this.getInitialValue(),
|
|
||||||
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
|
||||||
formShouldShowErrors: false,
|
|
||||||
validationErrors: [],
|
|
||||||
pendingValidation: Promise.resolve(),
|
|
||||||
// These registries are used for injected messages registrants only (mostly internal).
|
|
||||||
ruleRegistry: [],
|
|
||||||
messageRegistry: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...context,
|
|
||||||
classification () {
|
|
||||||
const classification = this.$formulate.classify(this.type)
|
|
||||||
return (classification === 'box' && this.options) ? 'group' : classification
|
|
||||||
},
|
|
||||||
component () {
|
|
||||||
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
|
|
||||||
},
|
|
||||||
parsedValidationRules () {
|
|
||||||
const parsedValidationRules = {}
|
|
||||||
Object.keys(this.validationRules).forEach(key => {
|
|
||||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
|
||||||
})
|
|
||||||
return parsedValidationRules
|
|
||||||
},
|
|
||||||
messages () {
|
|
||||||
const messages = {}
|
|
||||||
Object.keys(this.validationMessages).forEach((key) => {
|
|
||||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
|
||||||
})
|
|
||||||
Object.keys(this.messageRegistry).forEach((key) => {
|
|
||||||
messages[snakeToCamel(key)] = this.messageRegistry[key]
|
|
||||||
})
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$attrs': {
|
|
||||||
handler (value) {
|
|
||||||
this.updateLocalAttributes(value)
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
proxy (newValue, oldValue) {
|
|
||||||
this.performValidation()
|
|
||||||
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
|
||||||
this.context.model = newValue
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formulateValue (newValue, oldValue) {
|
|
||||||
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
|
||||||
this.context.model = newValue
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showValidationErrors: {
|
|
||||||
handler (val) {
|
|
||||||
this.$emit('error-visibility', val)
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.applyInitialValue()
|
|
||||||
if (this.formulateRegister && typeof this.formulateRegister === 'function') {
|
|
||||||
this.formulateRegister(this.nameOrFallback, this)
|
|
||||||
}
|
|
||||||
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
|
||||||
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
|
||||||
}
|
|
||||||
this.updateLocalAttributes(this.$attrs)
|
|
||||||
this.performValidation()
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
|
||||||
this.removeErrorObserver(this.setErrors)
|
|
||||||
}
|
|
||||||
if (typeof this.formulateDeregister === 'function') {
|
|
||||||
this.formulateDeregister(this.nameOrFallback)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getInitialValue () {
|
|
||||||
// Manually request classification, pre-computed props
|
|
||||||
var classification = this.$formulate.classify(this.type)
|
|
||||||
classification = (classification === 'box' && this.options) ? 'group' : classification
|
|
||||||
if (classification === 'box' && this.checked) {
|
|
||||||
return this.value || true
|
|
||||||
} else if (has(this.$options.propsData, 'value') && classification !== 'box') {
|
|
||||||
return this.value
|
|
||||||
} else if (has(this.$options.propsData, 'formulateValue')) {
|
|
||||||
return this.formulateValue
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
applyInitialValue () {
|
|
||||||
// This should only be run immediately on created and ensures that the
|
|
||||||
// proxy and the model are both the same before any additional registration.
|
|
||||||
if (
|
|
||||||
!shallowEqualObjects(this.context.model, this.proxy) &&
|
|
||||||
// we dont' want to set the model if we are a sub-box of a multi-box field
|
|
||||||
(Object.prototype.hasOwnProperty(this.$options.propsData, 'options') && this.classification === 'box')
|
|
||||||
) {
|
|
||||||
this.context.model = this.proxy
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateLocalAttributes (value) {
|
|
||||||
if (!shallowEqualObjects(value, this.localAttributes)) {
|
|
||||||
this.localAttributes = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
performValidation () {
|
|
||||||
let rules = parseRules(this.validation, this.$formulate.rules(this.parsedValidationRules))
|
|
||||||
// Add in ruleRegistry rules. These are added directly via injection from
|
|
||||||
// children and not part of the standard validation rule set.
|
|
||||||
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
|
||||||
this.pendingValidation = this.runRules(rules)
|
|
||||||
.then(messages => this.didValidate(messages))
|
|
||||||
return this.pendingValidation
|
|
||||||
},
|
|
||||||
runRules (rules) {
|
|
||||||
const run = ([rule, args, ruleName, modifier]) => {
|
|
||||||
var res = rule({
|
|
||||||
value: this.context.model,
|
|
||||||
getFormValues: this.getFormValues.bind(this),
|
|
||||||
name: this.context.name
|
|
||||||
}, ...args)
|
|
||||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
|
||||||
return res.then(result => result ? false : this.getMessage(ruleName, args))
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const resolveGroups = (groups, allMessages = []) => {
|
|
||||||
const ruleGroup = groups.shift()
|
|
||||||
if (Array.isArray(ruleGroup) && ruleGroup.length) {
|
|
||||||
Promise.all(ruleGroup.map(run))
|
|
||||||
.then(messages => messages.filter(m => !!m))
|
|
||||||
.then(messages => {
|
|
||||||
messages = Array.isArray(messages) ? messages : []
|
|
||||||
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
|
||||||
if ((!messages.length || !ruleGroup.bail) && groups.length) {
|
|
||||||
return resolveGroups(groups, allMessages.concat(messages))
|
|
||||||
}
|
|
||||||
return resolve(allMessages.concat(messages))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolve([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolveGroups(groupBails(rules))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
didValidate (messages) {
|
|
||||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
|
||||||
this.validationErrors = messages
|
|
||||||
if (validationChanged) {
|
|
||||||
const errorObject = this.getErrorObject()
|
|
||||||
this.$emit('validation', errorObject)
|
|
||||||
if (this.formulateFieldValidation && typeof this.formulateFieldValidation === 'function') {
|
|
||||||
this.formulateFieldValidation(errorObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getMessage (ruleName, args) {
|
|
||||||
return this.getMessageFunc(ruleName)({
|
|
||||||
args,
|
|
||||||
name: this.mergedValidationName,
|
|
||||||
value: this.context.model,
|
|
||||||
vm: this,
|
|
||||||
formValues: this.getFormValues()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getMessageFunc (ruleName) {
|
|
||||||
ruleName = snakeToCamel(ruleName)
|
|
||||||
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
|
||||||
switch (typeof this.messages[ruleName]) {
|
|
||||||
case 'function':
|
|
||||||
return this.messages[ruleName]
|
|
||||||
case 'string':
|
|
||||||
case 'boolean':
|
|
||||||
return () => this.messages[ruleName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (context) => this.$formulate.validationMessage(ruleName, context, this)
|
|
||||||
},
|
|
||||||
hasValidationErrors () {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getValidationErrors () {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getErrorObject () {
|
|
||||||
return {
|
|
||||||
name: this.context.nameOrFallback || this.context.name,
|
|
||||||
errors: this.validationErrors.filter(s => typeof s === 'string'),
|
|
||||||
hasErrors: !!this.validationErrors.length
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setErrors (errors) {
|
|
||||||
this.localErrors = arrayify(errors)
|
|
||||||
},
|
|
||||||
registerRule (rule, args, ruleName, message = null) {
|
|
||||||
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
|
|
||||||
// These are the raw rule format since they will be used directly.
|
|
||||||
this.ruleRegistry.push([rule, args, ruleName])
|
|
||||||
if (message !== null) {
|
|
||||||
this.messageRegistry[ruleName] = message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeRule (key) {
|
|
||||||
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
|
||||||
if (ruleIndex >= 0) {
|
|
||||||
this.ruleRegistry.splice(ruleIndex, 1)
|
|
||||||
delete this.messageRegistry[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Default base for input components.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
type () {
|
|
||||||
return this.context.type
|
|
||||||
},
|
|
||||||
id () {
|
|
||||||
return this.context.id
|
|
||||||
},
|
|
||||||
attributes () {
|
|
||||||
return this.context.attributes || {}
|
|
||||||
},
|
|
||||||
hasValue () {
|
|
||||||
return !!this.context.model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
<template>
|
|
||||||
<FormulateSlot
|
|
||||||
name="repeatable"
|
|
||||||
:context="context"
|
|
||||||
:index="index"
|
|
||||||
:remove-item="removeItem"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="context.slotComponents.repeatable"
|
|
||||||
:context="context"
|
|
||||||
:index="index"
|
|
||||||
:remove-item="removeItem"
|
|
||||||
>
|
|
||||||
<FormulateSlot
|
|
||||||
:context="context"
|
|
||||||
:index="index"
|
|
||||||
name="default"
|
|
||||||
/>
|
|
||||||
</component>
|
|
||||||
</FormulateSlot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
provide () {
|
|
||||||
return {
|
|
||||||
...useRegistryProviders(this),
|
|
||||||
formulateSetter: (field, value) => this.setFieldValue(field, value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inject: {
|
|
||||||
registerProvider: 'registerProvider',
|
|
||||||
deregisterProvider: 'deregisterProvider'
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
index: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
setFieldValue: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
...useRegistry(this),
|
|
||||||
isGrouping: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...useRegistryComputed()
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.registerProvider(this)
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
this.deregisterProvider(this)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...useRegistryMethods(['setFieldValue']),
|
|
||||||
removeItem () {
|
|
||||||
this.$emit('remove', this.index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
inheritAttrs: false,
|
|
||||||
functional: true,
|
|
||||||
render (h, { props, data, parent, children }) {
|
|
||||||
var p = parent
|
|
||||||
var { name, forceWrap, context, ...mergeWithContext } = props
|
|
||||||
|
|
||||||
// Look up the ancestor tree for the first FormulateInput
|
|
||||||
while (p && p.$options.name !== 'FormulateInput') {
|
|
||||||
p = p.$parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we never found the proper parent, just end it.
|
|
||||||
if (!p) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found a formulate input, check for a matching scoped slot
|
|
||||||
if (p.$scopedSlots && p.$scopedSlots[props.name]) {
|
|
||||||
return p.$scopedSlots[props.name]({ ...context, ...mergeWithContext })
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found no scoped slot, take the children and render those inside a wrapper if there are multiple
|
|
||||||
if (Array.isArray(children) && (children.length > 1 || (forceWrap && children.length > 0))) {
|
|
||||||
const { name, context, ...attrs } = data.attrs
|
|
||||||
return h('div', { ...data, ...{ attrs } }, children)
|
|
||||||
|
|
||||||
// If there is only one child, render it alone
|
|
||||||
} else if (Array.isArray(children) && children.length === 1) {
|
|
||||||
return children[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no children, render nothing
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="formulate-input-group"
|
|
||||||
:data-is-repeatable="context.repeatable"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-if="subType !== 'grouping'"
|
|
||||||
>
|
|
||||||
<FormulateInput
|
|
||||||
v-for="optionContext in optionsWithContext"
|
|
||||||
:key="optionContext.id"
|
|
||||||
v-model="context.model"
|
|
||||||
v-bind="optionContext"
|
|
||||||
:disable-errors="true"
|
|
||||||
class="formulate-input-group-item"
|
|
||||||
@blur="context.blurHandler"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
<FormulateGrouping
|
|
||||||
:context="context"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</FormulateGrouping>
|
|
||||||
<FormulateSlot
|
|
||||||
v-if="canAddMore"
|
|
||||||
name="addmore"
|
|
||||||
:context="context"
|
|
||||||
:add-more="addItem"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="context.slotComponents.addMore"
|
|
||||||
:context="context"
|
|
||||||
:add-more="addItem"
|
|
||||||
@add="addItem"
|
|
||||||
/>
|
|
||||||
</FormulateSlot>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { setId } from '../libs/utils'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FormulateInputGroup',
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
options () {
|
|
||||||
return this.context.options || []
|
|
||||||
},
|
|
||||||
subType () {
|
|
||||||
return (this.context.type === 'group') ? 'grouping' : 'inputs'
|
|
||||||
},
|
|
||||||
optionsWithContext () {
|
|
||||||
const {
|
|
||||||
// The following are a list of items to pull out of the context object
|
|
||||||
attributes: { id, ...groupApplicableAttributes },
|
|
||||||
blurHandler,
|
|
||||||
classification,
|
|
||||||
component,
|
|
||||||
getValidationErrors,
|
|
||||||
hasLabel,
|
|
||||||
hasValidationErrors,
|
|
||||||
isSubField,
|
|
||||||
labelPosition,
|
|
||||||
options,
|
|
||||||
performValidation,
|
|
||||||
setErrors,
|
|
||||||
slotComponents,
|
|
||||||
validationErrors,
|
|
||||||
visibleValidationErrors,
|
|
||||||
help,
|
|
||||||
...context
|
|
||||||
} = this.context
|
|
||||||
return this.options.map(option => this.groupItemContext(
|
|
||||||
context,
|
|
||||||
option,
|
|
||||||
groupApplicableAttributes
|
|
||||||
))
|
|
||||||
},
|
|
||||||
canAddMore () {
|
|
||||||
return (this.context.repeatable && this.items.length < this.context.limit)
|
|
||||||
},
|
|
||||||
items () {
|
|
||||||
return Array.isArray(this.context.model) ? this.context.model : [{}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addItem () {
|
|
||||||
if (Array.isArray(this.context.model)) {
|
|
||||||
this.context.model.push(setId({}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.context.model = this.items.concat([setId({})])
|
|
||||||
},
|
|
||||||
groupItemContext (context, option, groupAttributes) {
|
|
||||||
const optionAttributes = {}
|
|
||||||
const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes, !context.hasGivenName ? {
|
|
||||||
name: true
|
|
||||||
} : {})
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -8,25 +8,18 @@ import { map, arrayify, shallowEqualObjects } from './utils'
|
|||||||
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),
|
||||||
classification: this.classification,
|
|
||||||
component: this.component,
|
|
||||||
disableErrors: this.disableErrors,
|
disableErrors: this.disableErrors,
|
||||||
errors: this.explicitErrors,
|
errors: this.explicitErrors,
|
||||||
allErrors: this.allErrors,
|
allErrors: this.allErrors,
|
||||||
formShouldShowErrors: this.formShouldShowErrors,
|
formShouldShowErrors: this.formShouldShowErrors,
|
||||||
getValidationErrors: this.getValidationErrors.bind(this),
|
getValidationErrors: this.getValidationErrors.bind(this),
|
||||||
hasGivenName: this.hasGivenName,
|
hasGivenName: this.hasGivenName,
|
||||||
hasLabel: (this.label && this.classification !== 'button'),
|
|
||||||
hasValidationErrors: this.hasValidationErrors.bind(this),
|
hasValidationErrors: this.hasValidationErrors.bind(this),
|
||||||
help: this.help,
|
help: this.help,
|
||||||
helpPosition: this.logicalHelpPosition,
|
|
||||||
id: this.id || this.defaultId,
|
id: this.id || this.defaultId,
|
||||||
imageBehavior: this.imageBehavior,
|
imageBehavior: this.imageBehavior,
|
||||||
label: this.label,
|
|
||||||
labelPosition: this.logicalLabelPosition,
|
|
||||||
limit: this.limit,
|
limit: this.limit,
|
||||||
name: this.nameOrFallback,
|
name: this.nameOrFallback,
|
||||||
performValidation: this.performValidation.bind(this),
|
performValidation: this.performValidation.bind(this),
|
||||||
@ -34,25 +27,18 @@ export default {
|
|||||||
repeatable: this.repeatable,
|
repeatable: this.repeatable,
|
||||||
setErrors: this.setErrors.bind(this),
|
setErrors: this.setErrors.bind(this),
|
||||||
showValidationErrors: this.showValidationErrors,
|
showValidationErrors: this.showValidationErrors,
|
||||||
slotComponents: this.slotComponents,
|
|
||||||
type: this.type,
|
|
||||||
uploadBehavior: this.uploadBehavior,
|
uploadBehavior: this.uploadBehavior,
|
||||||
uploadUrl: this.mergedUploadUrl,
|
uploadUrl: this.mergedUploadUrl,
|
||||||
uploader: this.uploader || this.$formulate.getUploader(),
|
uploader: this.uploader || this.$formulario.getUploader(),
|
||||||
validationErrors: this.validationErrors,
|
validationErrors: this.validationErrors,
|
||||||
value: this.value,
|
value: this.value,
|
||||||
visibleValidationErrors: this.visibleValidationErrors,
|
visibleValidationErrors: this.visibleValidationErrors,
|
||||||
isSubField: this.isSubField,
|
|
||||||
...this.typeContext
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// Used in sub-context
|
// Used in sub-context
|
||||||
nameOrFallback,
|
nameOrFallback,
|
||||||
hasGivenName,
|
hasGivenName,
|
||||||
typeContext,
|
|
||||||
elementAttributes,
|
elementAttributes,
|
||||||
logicalLabelPosition,
|
|
||||||
logicalHelpPosition,
|
|
||||||
mergedUploadUrl,
|
mergedUploadUrl,
|
||||||
|
|
||||||
// These items are not passed as context
|
// These items are not passed as context
|
||||||
@ -63,44 +49,7 @@ export default {
|
|||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,37 +77,6 @@ function elementAttributes () {
|
|||||||
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.
|
||||||
*/
|
*/
|
||||||
@ -166,13 +84,8 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,9 +104,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,12 +120,10 @@ function visibleValidationErrors () {
|
|||||||
* Return the element’s name, or select a fallback.
|
* Return the element’s name, or select a fallback.
|
||||||
*/
|
*/
|
||||||
function nameOrFallback () {
|
function nameOrFallback () {
|
||||||
if (this.name === true && this.classification !== 'button') {
|
if (this.path !== '') {
|
||||||
return `${this.type}_${this.elementAttributes.id}`
|
return this.path + '.' + this.name
|
||||||
}
|
|
||||||
if (this.name === false || (this.classification === 'button' && this.name === true)) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.name
|
return this.name
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,10 +135,10 @@ function hasGivenName () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
||||||
@ -284,20 +193,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
@ -323,11 +218,8 @@ function defineModel (context) {
|
|||||||
* 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 []
|
|
||||||
}
|
|
||||||
if (!this[model]) {
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return this[model]
|
return this[model]
|
||||||
@ -341,7 +233,7 @@ function modelSetter (value) {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* 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)))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
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
|
||||||
@ -78,19 +78,19 @@ class Registry {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
this.registry.set(field, component)
|
this.registry.set(field, component)
|
||||||
const hasVModelValue = has(component.$options.propsData, 'formulateValue')
|
const hasVModelValue = has(component.$options.propsData, 'formularioValue')
|
||||||
const hasValue = has(component.$options.propsData, 'value')
|
const hasValue = has(component.$options.propsData, 'value')
|
||||||
if (
|
if (
|
||||||
!hasVModelValue &&
|
!hasVModelValue &&
|
||||||
this.ctx.hasInitialValue &&
|
this.ctx.hasInitialValue &&
|
||||||
this.ctx.initialValues[field]
|
getNested(this.ctx.initialValues, field) !== undefined
|
||||||
) {
|
) {
|
||||||
// In the case that the form is carrying an initial value and the
|
// In the case that the form is carrying an initial value and the
|
||||||
// element is not, set it directly.
|
// element is not, set it directly.
|
||||||
component.context.model = this.ctx.initialValues[field]
|
component.context.model = getNested(this.ctx.initialValues, field)
|
||||||
} else if (
|
} else if (
|
||||||
(hasVModelValue || hasValue) &&
|
(hasVModelValue || hasValue) &&
|
||||||
!shallowEqualObjects(component.proxy, this.ctx.initialValues[field])
|
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
||||||
) {
|
) {
|
||||||
// In this case, the field is v-modeled or has an initial value and the
|
// 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
|
// form has no value or a different value, so use the field value
|
||||||
@ -142,24 +142,24 @@ 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'
|
||||||
@ -191,7 +191,7 @@ export function useRegistryMethods (without = []) {
|
|||||||
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))
|
||||||
},
|
},
|
||||||
@ -222,11 +222,11 @@ export function useRegistryMethods (without = []) {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -241,9 +241,9 @@ export function useRegistryMethods (without = []) {
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -284,3 +284,63 @@ export function has (ctx, prop) {
|
|||||||
export function setId (o, id) {
|
export function setId (o, id) {
|
||||||
return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') }))
|
return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNested(obj, field) {
|
||||||
|
let fieldParts = field.split('.');
|
||||||
|
|
||||||
|
let result = obj;
|
||||||
|
for (const key in fieldParts) {
|
||||||
|
let matches = fieldParts[key].match(/(.+)\[(\d+)\]$/);
|
||||||
|
if (result === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
result = result[matches[1]];
|
||||||
|
|
||||||
|
if (result === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
result = result[matches[2]];
|
||||||
|
} else {
|
||||||
|
result = result[fieldParts[key]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNested(obj, field, value) {
|
||||||
|
let fieldParts = field.split('.');
|
||||||
|
|
||||||
|
let subProxy = obj;
|
||||||
|
for (let i = 0; i < fieldParts.length; i++) {
|
||||||
|
let fieldPart = fieldParts[i];
|
||||||
|
|
||||||
|
let matches = fieldPart.match(/(.+)\[(\d+)\]$/);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
if (subProxy[matches[1]] === undefined) {
|
||||||
|
subProxy[matches[1]] = [];
|
||||||
|
}
|
||||||
|
subProxy = subProxy[matches[1]];
|
||||||
|
|
||||||
|
if (i == fieldParts.length - 1) {
|
||||||
|
subProxy[matches[2]] = value
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
subProxy = subProxy[matches[2]];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i == fieldParts.length - 1) {
|
||||||
|
subProxy[fieldPart] = value
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (subProxy[fieldPart] === undefined) {
|
||||||
|
subProxy[fieldPart] = {};
|
||||||
|
}
|
||||||
|
subProxy = subProxy[fieldPart];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
# i18n moved
|
|
||||||
|
|
||||||
Locales have been removed from vue-formulate core to [vue-formulate-i18n](https://github.com/wearebraid/vue-formulate-i18n).
|
|
||||||
This was done to allow for better tree-shaking by bundlers and allow
|
|
||||||
for lots of additional language support without increasing the size of the core package.
|
|
||||||
|
|
||||||
## PRs welcome
|
|
||||||
|
|
||||||
[Please read the i18n contribution documentation](https://www.vueformulate.com/guide/contributing/#internationalization).
|
|
@ -1,26 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="formulate-input-group-add-more">
|
|
||||||
<FormulateInput
|
|
||||||
type="button"
|
|
||||||
:label="context.addLabel"
|
|
||||||
data-minor
|
|
||||||
data-ghost
|
|
||||||
@click="addMore"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
addMore: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="context.help"
|
|
||||||
:id="`${context.id}-help`"
|
|
||||||
:class="`formulate-input-help formulate-input-help--${context.helpPosition}`"
|
|
||||||
v-text="context.help"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,18 +0,0 @@
|
|||||||
<template>
|
|
||||||
<label
|
|
||||||
:class="`formulate-input-label formulate-input-label--${context.labelPosition}`"
|
|
||||||
:for="context.id"
|
|
||||||
v-text="context.label"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="formulate-input-group-repeatable"
|
|
||||||
>
|
|
||||||
<FormulateSlot
|
|
||||||
name="remove"
|
|
||||||
:context="context"
|
|
||||||
:remove-item="removeItem"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="context.slotComponents.remove"
|
|
||||||
:context="context"
|
|
||||||
:remove-item="removeItem"
|
|
||||||
/>
|
|
||||||
</FormulateSlot>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
removeItem: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a
|
|
||||||
v-if="context.repeatable"
|
|
||||||
class="formulate-input-group-repeatable-remove"
|
|
||||||
role="button"
|
|
||||||
@click.prevent="removeItem"
|
|
||||||
@keypress.enter="removeItem"
|
|
||||||
v-text="`Remove`"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
removeItem: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
Loading…
Reference in New Issue
Block a user