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

Library renamed to formulario

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

View File

@ -10,5 +10,5 @@ assignees: ''
**Describe the new feature you'd like**
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%

View File

@ -1,5 +1,6 @@
MIT License
Copyright (c) 2020 RetailDriver LLC
Copyright (c) 2020 Braid LLC
Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -5,16 +5,15 @@ import vue from 'rollup-plugin-vue' // Handle .vue SFC files
import { terser } from 'rollup-plugin-terser'
export default {
input: 'src/Formulate.js', // Path relative to package.json
input: 'src/Formulario.js', // Path relative to package.json
output: [
{
name: 'Formulate',
name: 'Formulario',
exports: 'default',
globals: {
'is-plain-object': 'isPlainObject',
'nanoid/non-secure': 'nanoid',
'is-url': 'isUrl',
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
},
sourcemap: false
}

View File

@ -6,16 +6,15 @@ import internal from 'rollup-plugin-internal'
import { terser } from 'rollup-plugin-terser'
export default {
input: 'src/Formulate.js', // Path relative to package.json
input: 'src/Formulario.js', // Path relative to package.json
output: {
name: 'VueFormulate',
name: 'VueFormulario',
exports: 'default',
format: 'iife',
globals: {
'is-plain-object': 'isPlainObject',
'nanoid/non-secure': 'nanoid',
'is-url': 'isUrl',
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
}
},
plugins: [
@ -24,7 +23,7 @@ export default {
preferBuiltins: false
}),
commonjs(),
internal(['is-plain-object', 'nanoid/non-secure', 'is-url', '@braid/vue-formulate-i18n']),
internal(['is-plain-object', 'nanoid/non-secure', 'is-url']),
vue({
css: true, // Dynamically inject css as a <style> tag
compileTemplate: true // Explicitly convert template to render function

View File

@ -1,45 +1,45 @@
{
"name": "@braid/vue-formulate",
"version": "2.3.0",
"description": "The easiest way to build forms in Vue.",
"main": "dist/formulate.umd.js",
"module": "dist/formulate.esm.js",
"unpkg": "dist/formulate.min.js",
"name": "@retailcrm/vue-formulario",
"version": "0.1.0",
"main": "dist/formulario.umd.js",
"module": "dist/formulario.esm.js",
"unpkg": "dist/formulario.min.js",
"publishConfig": {
"access": "public"
},
"browser": {
"./sfc": "src/Formulate.js"
"./sfc": "src/Formulario.js"
},
"scripts": {
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulate.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulate.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulate.min.js | wc -c)b gzip\"",
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulate.esm.js",
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulate.umd.js",
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulate.min.js",
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulario.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulario.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulario.min.js | wc -c)b gzip\"",
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js",
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js",
"test": "NODE_ENV=test jest --config test/jest.conf.js",
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
"build:size": "gzip -c dist/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"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/wearebraid/vue-formulate.git"
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
},
"keywords": [
"vue",
"form",
"forms",
"validation",
"vuex",
"validate"
],
"author": "Justin Schroeder <justin@wearebraid.com>",
"author": "RetailDriverLLC <integration@retailcrm.ru>",
"contributors": [
"Justin Schroeder <justin@wearebraid.com>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/wearebraid/vue-formulate/issues"
"url": "https://github.com/retailcrm/vue-formulario/issues"
},
"homepage": "https://www.vueformulate.com",
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
@ -81,7 +81,6 @@
"watch": "^1.0.2"
},
"dependencies": {
"@braid/vue-formulate-i18n": "^1.6.2",
"is-plain-object": "^3.0.0",
"is-url": "^1.2.4",
"nanoid": "^2.1.11"

View File

@ -3,8 +3,8 @@ import FileUpload from './FileUpload'
export default class FormSubmission {
/**
* Initialize a formulate form.
* @param {vm} form an instance of FormulateForm
* Initialize a formulario form.
* @param {vm} form an instance of FormularioForm
*/
constructor (form) {
this.form = form

276
src/Formulario.js Normal file
View File

@ -0,0 +1,276 @@
import library from './libs/library'
import rules from './libs/rules'
import mimes from './libs/mimes'
import FileUpload from './FileUpload'
import { arrayify, parseLocale, has } from './libs/utils'
import isPlainObject from 'is-plain-object'
import fauxUploader from './libs/faux-uploader'
import FormularioForm from './FormularioForm.vue'
import FormularioInput from './FormularioInput.vue'
import FormularioGrouping from './FormularioGrouping.vue'
/**
* The base formulario library.
*/
class Formulario {
/**
* Instantiate our base options.
*/
constructor () {
this.options = {}
this.defaults = {
components: {
FormularioForm,
FormularioInput,
FormularioGrouping,
},
library,
rules,
mimes,
locale: false,
uploader: fauxUploader,
uploadUrl: false,
fileUrlKey: 'url',
uploadJustCompleteDuration: 1000,
errorHandler: (err) => err,
plugins: [],
idPrefix: 'formulario-'
}
this.registry = new Map()
this.idRegistry = {}
}
/**
* Install vue formulario, and register its components.
*/
install (Vue, options) {
Vue.prototype.$formulario = this
this.options = this.defaults
var plugins = this.defaults.plugins
if (options && Array.isArray(options.plugins) && options.plugins.length) {
plugins = plugins.concat(options.plugins)
}
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
this.extend(options || {})
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName])
}
}
/**
* Produce a deterministically generated id based on the sequence by which it
* was requested. This should be *theoretically* the same SSR as client side.
* However, SSR and deterministic ids can be very challenging, so this
* implementation is open to community review.
*/
nextId (vm) {
const path = vm.$route && vm.$route.path ? vm.$route.path : false
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
this.idRegistry[pathPrefix] = 0
}
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
}
/**
* Given a set of options, apply them to the pre-existing options.
* @param {Object} extendWith
*/
extend (extendWith) {
if (typeof extendWith === 'object') {
this.options = this.merge(this.options, extendWith)
return this
}
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
}
/**
* Create a new object by copying properties of base and mergeWith.
* Note: arrays don't overwrite - they push
*
* @param {Object} base
* @param {Object} mergeWith
* @param {boolean} concatArrays
*/
merge (base, mergeWith, concatArrays = true) {
var merged = {}
for (var key in base) {
if (mergeWith.hasOwnProperty(key)) {
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
merged[key] = base[key].concat(mergeWith[key])
} else {
merged[key] = mergeWith[key]
}
} else {
merged[key] = base[key]
}
}
for (var prop in mergeWith) {
if (!merged.hasOwnProperty(prop)) {
merged[prop] = mergeWith[prop]
}
}
return merged
}
/**
* Determine what "class" of input this element is given the "type".
* @param {string} type
*/
classify (type) {
if (this.options.library.hasOwnProperty(type)) {
return this.options.library[type].classification
}
return 'unknown'
}
/**
* Determine what type of component to render given the "type".
* @param {string} type
*/
component (type) {
if (this.options.library.hasOwnProperty(type)) {
return this.options.library[type].component
}
return false
}
/**
* Get validation rules by merging any passed in with global rules.
* @return {object} object of validation functions
*/
rules (rules = {}) {
return { ...this.options.rules, ...rules }
}
/**
* Attempt to get the vue-i18n configured locale.
*/
i18n (vm) {
if (vm.$i18n) {
switch (typeof vm.$i18n.locale) {
case 'string':
return vm.$i18n.locale
case 'function':
return vm.$i18n.locale()
}
}
return false
}
/**
* Get the validation message for a particular error.
*/
validationMessage (rule, validationContext, vm) {
return rule
}
/**
* Given an instance of a FormularioForm register it.
* @param {vm} form
*/
register (form) {
if (form.$options.name === 'FormularioForm' && form.name) {
this.registry.set(form.name, form)
}
}
/**
* Given an instance of a form, remove it from the registry.
* @param {vm} form
*/
deregister (form) {
if (
form.$options.name === 'FormularioForm' &&
form.name &&
this.registry.has(form.name)
) {
this.registry.delete(form.name)
}
}
/**
* Given an array, this function will attempt to make sense of the given error
* and hydrate a form with the resulting errors.
*
* @param {error} err
* @param {string} formName
* @param {error}
*/
handle (err, formName, skip = false) {
const e = skip ? err : this.options.errorHandler(err, formName)
if (formName && this.registry.has(formName)) {
this.registry.get(formName).applyErrors({
formErrors: arrayify(e.formErrors),
inputErrors: e.inputErrors || {}
})
}
return e
}
/**
* Reset a form.
* @param {string} formName
* @param {object} initialValue
*/
reset (formName, initialValue = {}) {
this.resetValidation(formName)
this.setValues(formName, initialValue)
}
/**
* Reset the form's validation messages.
* @param {string} formName
*/
resetValidation (formName) {
const form = this.registry.get(formName)
form.hideErrors(formName)
form.namedErrors = []
form.namedFieldErrors = {}
}
/**
* Set the form values.
* @param {string} formName
* @param {object} values
*/
setValues (formName, values) {
if (values && !Array.isArray(values) && typeof values === 'object') {
const form = this.registry.get(formName)
form.setValues({ ...values })
}
}
/**
* Get the file uploader.
*/
getUploader () {
return this.options.uploader || false
}
/**
* Get the global upload url.
*/
getUploadUrl () {
return this.options.uploadUrl || false
}
/**
* When re-hydrating a file uploader with an array, get the sub-object key to
* access the url of the file. Usually this is just "url".
*/
getFileUrlKey () {
return this.options.fileUrlKey || 'url'
}
/**
* Create a new instance of an upload.
*/
createUpload (fileList, context) {
return new FileUpload(fileList, context, this.options)
}
}
export default new Formulario()

85
src/FormularioFiles.vue Normal file
View File

@ -0,0 +1,85 @@
<template>
<ul
v-if="fileUploads.length"
class="formulario-files"
>
<li
v-for="file in fileUploads"
:key="file.uuid"
:data-has-error="!!file.error"
:data-has-preview="!!(imagePreview && file.previewData)"
>
<div class="formulario-file">
<div
v-if="!!(imagePreview && file.previewData)"
class="formulario-file-image-preview"
>
<img
:src="file.previewData"
>
</div>
<div
class="formulario-file-name"
:title="file.name"
v-text="file.name"
/>
<div
v-if="file.progress !== false"
:data-just-finished="file.justFinished"
:data-is-finished="!file.justFinished && file.complete"
class="formulario-file-progress"
>
<div
class="formulario-file-progress-inner"
:style="{width: file.progress + '%'}"
/>
</div>
<div
v-if="(file.complete && !file.justFinished) || file.progress === false"
class="formulario-file-remove"
@click="file.removeFile"
/>
</div>
<div
v-if="file.error"
class="formulario-file-upload-error"
v-text="file.error"
/>
</li>
</ul>
</template>
<script>
import FileUpload from './FileUpload'
export default {
name: 'FormularioFiles',
props: {
files: {
type: FileUpload,
required: true
},
imagePreview: {
type: Boolean,
default: false
}
},
computed: {
fileUploads () {
return this.files.files || []
}
},
watch: {
files () {
if (this.imagePreview) {
this.files.loadPreviews()
}
}
},
mounted () {
if (this.imagePreview) {
this.files.loadPreviews()
}
}
}
</script>

167
src/FormularioForm.vue Normal file
View File

@ -0,0 +1,167 @@
<template>
<form
:class="classes"
@submit.prevent="formSubmitted"
>
<slot :errors="mergedFormErrors" />
</form>
</template>
<script>
import { arrayify, has } from './libs/utils'
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
import FormSubmission from './FormSubmission'
export default {
provide () {
return {
...useRegistryProviders(this),
observeErrors: this.addErrorObserver,
removeErrorObserver: this.removeErrorObserver,
formularioFieldValidation: this.formularioFieldValidation,
path: ''
}
},
name: 'FormularioForm',
model: {
prop: 'formularioValue',
event: 'input'
},
props: {
name: {
type: [String, Boolean],
default: false
},
formularioValue: {
type: Object,
default: () => ({})
},
values: {
type: [Object, Boolean],
default: false
},
errors: {
type: [Object, Boolean],
default: false
},
formErrors: {
type: Array,
default: () => ([])
}
},
data () {
return {
...useRegistry(this),
formShouldShowErrors: false,
errorObservers: [],
namedErrors: [],
namedFieldErrors: {}
}
},
computed: {
...useRegistryComputed(),
classes () {
const classes = { 'formulario-form': true }
if (this.name) {
classes[`formulario-form--${this.name}`] = true
}
return classes
},
mergedFormErrors () {
return this.formErrors.concat(this.namedErrors)
},
mergedFieldErrors () {
const errors = {}
if (this.errors) {
for (const fieldName in this.errors) {
errors[fieldName] = arrayify(this.errors[fieldName])
}
}
for (const fieldName in this.namedFieldErrors) {
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
}
return errors
},
hasFormErrorObservers () {
return !!this.errorObservers.filter(o => o.type === 'form').length
}
},
watch: {
formularioValue: {
handler (values) {
if (this.isVmodeled &&
values &&
typeof values === 'object'
) {
this.setValues(values)
}
},
deep: true
},
mergedFormErrors (errors) {
this.errorObservers
.filter(o => o.type === 'form')
.forEach(o => o.callback(errors))
},
mergedFieldErrors: {
handler (errors) {
this.errorObservers
.filter(o => o.type === 'input')
.forEach(o => o.callback(errors[o.field] || []))
},
immediate: true
}
},
created () {
this.$formulario.register(this)
this.applyInitialValues()
},
destroyed () {
this.$formulario.deregister(this)
},
methods: {
...useRegistryMethods(),
applyErrors ({ formErrors, inputErrors }) {
// given an object of errors, apply them to this form
this.namedErrors = formErrors
this.namedFieldErrors = inputErrors
},
addErrorObserver (observer) {
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
this.errorObservers.push(observer)
if (observer.type === 'form') {
observer.callback(this.mergedFormErrors)
} else if (has(this.mergedFieldErrors, observer.field)) {
observer.callback(this.mergedFieldErrors[observer.field])
}
}
},
removeErrorObserver (observer) {
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
},
registerErrorComponent (component) {
if (!this.errorComponents.includes(component)) {
this.errorComponents.push(component)
}
},
formSubmitted () {
// perform validation here
this.showErrors()
const submission = new FormSubmission(this)
this.$emit('submit-raw', submission)
return submission.hasValidationErrors()
.then(hasErrors => hasErrors ? undefined : submission.values())
.then(data => {
if (typeof data !== 'undefined') {
this.$emit('submit', data)
return data
}
return undefined
})
},
formularioFieldValidation (errorObject) {
this.$emit('validation', errorObject)
}
}
}
</script>

View File

@ -0,0 +1,43 @@
<template>
<div
class="formulario-group"
data-type="group"
>
<slot />
</div>
</template>
<script>
export default {
name: 'FormularioGrouping',
props: {
name: {
type: String,
required: true
},
isArrayItem: {
type: Boolean,
default: false
},
},
provide () {
return {
path: this.groupPath
}
},
inject: ['path'],
computed: {
groupPath () {
if (this.isArrayItem) {
return this.path + '[' + this.name + ']';
} else {
if (this.path === '') {
return this.name;
}
return this.path + '.' + this.name;
}
}
}
}
</script>

331
src/FormularioInput.vue Normal file
View File

@ -0,0 +1,331 @@
<template>
<div
class="formulario-input"
:data-has-errors="hasErrors"
:data-is-showing-errors="hasVisibleErrors"
:data-type="type"
>
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
</div>
</template>
<script>
import context from './libs/context'
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
export default {
name: 'FormularioInput',
inheritAttrs: false,
provide () {
return {
// Allows sub-components of this input to register arbitrary rules.
formularioRegisterRule: this.registerRule,
formularioRemoveRule: this.removeRule
}
},
inject: {
formularioSetter: { default: undefined },
formularioFieldValidation: { default: () => () => ({}) },
formularioRegister: { default: undefined },
formularioDeregister: { default: undefined },
getFormValues: { default: () => () => ({}) },
observeErrors: { default: undefined },
removeErrorObserver: { default: undefined },
path: { default: '' }
},
model: {
prop: 'formularioValue',
event: 'input'
},
props: {
type: {
type: String,
default: 'text'
},
name: {
type: String,
required: true
},
/* eslint-disable */
formularioValue: {
default: ''
},
value: {
default: false
},
/* eslint-enable */
id: {
type: [String, Boolean, Number],
default: false
},
errors: {
type: [String, Array, Boolean],
default: false
},
validation: {
type: [String, Boolean, Array],
default: false
},
validationName: {
type: [String, Boolean],
default: false
},
errorBehavior: {
type: String,
default: 'blur',
validator: function (value) {
return ['blur', 'live', 'submit'].includes(value)
}
},
showErrors: {
type: Boolean,
default: false
},
imageBehavior: {
type: String,
default: 'preview'
},
uploadUrl: {
type: [String, Boolean],
default: false
},
uploader: {
type: [Function, Object, Boolean],
default: false
},
uploadBehavior: {
type: String,
default: 'live'
},
preventWindowDrops: {
type: Boolean,
default: true
},
validationMessages: {
type: Object,
default: () => ({})
},
validationRules: {
type: Object,
default: () => ({})
},
disableErrors: {
type: Boolean,
default: false
}
},
data () {
return {
defaultId: this.$formulario.nextId(this),
localAttributes: {},
localErrors: [],
proxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false,
validationErrors: [],
pendingValidation: Promise.resolve(),
// These registries are used for injected messages registrants only (mostly internal).
ruleRegistry: [],
messageRegistry: {}
}
},
computed: {
...context,
parsedValidationRules () {
const parsedValidationRules = {}
Object.keys(this.validationRules).forEach(key => {
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
})
return parsedValidationRules
},
messages () {
const messages = {}
Object.keys(this.validationMessages).forEach((key) => {
messages[snakeToCamel(key)] = this.validationMessages[key]
})
Object.keys(this.messageRegistry).forEach((key) => {
messages[snakeToCamel(key)] = this.messageRegistry[key]
})
return messages
}
},
watch: {
'$attrs': {
handler (value) {
this.updateLocalAttributes(value)
},
deep: true
},
proxy (newValue, oldValue) {
this.performValidation()
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
formularioValue (newValue, oldValue) {
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
showValidationErrors: {
handler (val) {
this.$emit('error-visibility', val)
},
immediate: true
}
},
created () {
this.applyInitialValue()
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
this.formularioRegister(this.nameOrFallback, this)
}
if (!this.disableErrors && typeof this.observeErrors === 'function') {
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
}
this.updateLocalAttributes(this.$attrs)
this.performValidation()
},
beforeDestroy () {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
this.removeErrorObserver(this.setErrors)
}
if (typeof this.formularioDeregister === 'function') {
this.formularioDeregister(this.nameOrFallback)
}
},
methods: {
getInitialValue () {
if (has(this.$options.propsData, 'value')) {
return this.value
} else if (has(this.$options.propsData, 'formularioValue')) {
return this.formularioValue
}
return ''
},
applyInitialValue () {
// This should only be run immediately on created and ensures that the
// proxy and the model are both the same before any additional registration.
if (!shallowEqualObjects(this.context.model, this.proxy)) {
this.context.model = this.proxy
}
},
updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value
}
},
performValidation () {
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
// Add in ruleRegistry rules. These are added directly via injection from
// children and not part of the standard validation rule set.
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
this.pendingValidation = this.runRules(rules)
.then(messages => this.didValidate(messages))
return this.pendingValidation
},
runRules (rules) {
const run = ([rule, args, ruleName, modifier]) => {
var res = rule({
value: this.context.model,
getFormValues: this.getFormValues.bind(this),
name: this.context.name
}, ...args)
res = (res instanceof Promise) ? res : Promise.resolve(res)
return res.then(result => result ? false : this.getMessage(ruleName, args))
}
return new Promise(resolve => {
const resolveGroups = (groups, allMessages = []) => {
const ruleGroup = groups.shift()
if (Array.isArray(ruleGroup) && ruleGroup.length) {
Promise.all(ruleGroup.map(run))
.then(messages => messages.filter(m => !!m))
.then(messages => {
messages = Array.isArray(messages) ? messages : []
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
if ((!messages.length || !ruleGroup.bail) && groups.length) {
return resolveGroups(groups, allMessages.concat(messages))
}
return resolve(allMessages.concat(messages))
})
} else {
resolve([])
}
}
resolveGroups(groupBails(rules))
})
},
didValidate (messages) {
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
this.validationErrors = messages
if (validationChanged) {
const errorObject = this.getErrorObject()
this.$emit('validation', errorObject)
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
this.formularioFieldValidation(errorObject)
}
}
},
getMessage (ruleName, args) {
return this.getMessageFunc(ruleName)({
args,
name: this.mergedValidationName,
value: this.context.model,
vm: this,
formValues: this.getFormValues()
})
},
getMessageFunc (ruleName) {
ruleName = snakeToCamel(ruleName)
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
switch (typeof this.messages[ruleName]) {
case 'function':
return this.messages[ruleName]
case 'string':
case 'boolean':
return () => this.messages[ruleName]
}
}
return (context) => this.$formulario.validationMessage(ruleName, context, this)
},
hasValidationErrors () {
return new Promise(resolve => {
this.$nextTick(() => {
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
})
})
},
getValidationErrors () {
return new Promise(resolve => {
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
})
},
getErrorObject () {
return {
name: this.context.nameOrFallback || this.context.name,
errors: this.validationErrors.filter(s => typeof s === 'string'),
hasErrors: !!this.validationErrors.length
}
},
setErrors (errors) {
this.localErrors = arrayify(errors)
},
registerRule (rule, args, ruleName, message = null) {
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
// These are the raw rule format since they will be used directly.
this.ruleRegistry.push([rule, args, ruleName])
if (message !== null) {
this.messageRegistry[ruleName] = message
}
}
},
removeRule (key) {
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
if (ruleIndex >= 0) {
this.ruleRegistry.splice(ruleIndex, 1)
delete this.messageRegistry[key]
}
}
}
}
</script>

View File

@ -1,352 +0,0 @@
import library from './libs/library'
import rules from './libs/rules'
import mimes from './libs/mimes'
import FileUpload from './FileUpload'
import { arrayify, parseLocale, has } from './libs/utils'
import isPlainObject from 'is-plain-object'
import { en } from '@braid/vue-formulate-i18n'
import fauxUploader from './libs/faux-uploader'
import FormulateSlot from './FormulateSlot'
import FormulateForm from './FormulateForm.vue'
import FormulateInput from './FormulateInput.vue'
import FormulateErrors from './FormulateErrors.vue'
import FormulateHelp from './slots/FormulateHelp.vue'
import FormulateGrouping from './FormulateGrouping.vue'
import FormulateLabel from './slots/FormulateLabel.vue'
import FormulateAddMore from './slots/FormulateAddMore.vue'
import FormulateRepeatable from './slots/FormulateRepeatable.vue'
import FormulateInputGroup from './inputs/FormulateInputGroup.vue'
import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue'
import FormulateRepeatableRemove from './slots/FormulateRepeatableRemove.vue'
/**
* The base formulate library.
*/
class Formulate {
/**
* Instantiate our base options.
*/
constructor () {
this.options = {}
this.defaults = {
components: {
FormulateSlot,
FormulateForm,
FormulateHelp,
FormulateLabel,
FormulateInput,
FormulateErrors,
FormulateAddMore,
FormulateGrouping,
FormulateRepeatable,
FormulateInputGroup,
FormulateRepeatableRemove,
FormulateRepeatableProvider
},
slotComponents: {
label: 'FormulateLabel',
help: 'FormulateHelp',
errors: 'FormulateErrors',
repeatable: 'FormulateRepeatable',
addMore: 'FormulateAddMore',
remove: 'FormulateRepeatableRemove'
},
library,
rules,
mimes,
locale: false,
uploader: fauxUploader,
uploadUrl: false,
fileUrlKey: 'url',
uploadJustCompleteDuration: 1000,
errorHandler: (err) => err,
plugins: [ en ],
locales: {},
idPrefix: 'formulate-'
}
this.registry = new Map()
this.idRegistry = {}
}
/**
* Install vue formulate, and register its components.
*/
install (Vue, options) {
Vue.prototype.$formulate = this
this.options = this.defaults
var plugins = this.defaults.plugins
if (options && Array.isArray(options.plugins) && options.plugins.length) {
plugins = plugins.concat(options.plugins)
}
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
this.extend(options || {})
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName])
}
}
/**
* Produce a deterministically generated id based on the sequence by which it
* was requested. This should be *theoretically* the same SSR as client side.
* However, SSR and deterministic ids can be very challenging, so this
* implementation is open to community review.
*/
nextId (vm) {
const path = vm.$route && vm.$route.path ? vm.$route.path : false
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
this.idRegistry[pathPrefix] = 0
}
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
}
/**
* Given a set of options, apply them to the pre-existing options.
* @param {Object} extendWith
*/
extend (extendWith) {
if (typeof extendWith === 'object') {
this.options = this.merge(this.options, extendWith)
return this
}
throw new Error(`VueFormulate extend() should be passed an object (was ${typeof extendWith})`)
}
/**
* Create a new object by copying properties of base and mergeWith.
* Note: arrays don't overwrite - they push
*
* @param {Object} base
* @param {Object} mergeWith
* @param {boolean} concatArrays
*/
merge (base, mergeWith, concatArrays = true) {
var merged = {}
for (var key in base) {
if (mergeWith.hasOwnProperty(key)) {
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
merged[key] = base[key].concat(mergeWith[key])
} else {
merged[key] = mergeWith[key]
}
} else {
merged[key] = base[key]
}
}
for (var prop in mergeWith) {
if (!merged.hasOwnProperty(prop)) {
merged[prop] = mergeWith[prop]
}
}
return merged
}
/**
* Determine what "class" of input this element is given the "type".
* @param {string} type
*/
classify (type) {
if (this.options.library.hasOwnProperty(type)) {
return this.options.library[type].classification
}
return 'unknown'
}
/**
* Determine what type of component to render given the "type".
* @param {string} type
*/
component (type) {
if (this.options.library.hasOwnProperty(type)) {
return this.options.library[type].component
}
return false
}
/**
* What component should be rendered for the given slot location and type.
* @param {string} type the type of component
* @param {string} slot the name of the slot
*/
slotComponent (type, slot) {
const def = this.options.library[type]
if (def && def.slotComponents && def.slotComponents[slot]) {
return def.slotComponents[slot]
}
return this.options.slotComponents[slot]
}
/**
* Get validation rules by merging any passed in with global rules.
* @return {object} object of validation functions
*/
rules (rules = {}) {
return { ...this.options.rules, ...rules }
}
/**
* Attempt to get the vue-i18n configured locale.
*/
i18n (vm) {
if (vm.$i18n) {
switch (typeof vm.$i18n.locale) {
case 'string':
return vm.$i18n.locale
case 'function':
return vm.$i18n.locale()
}
}
return false
}
/**
* Select the proper locale to use.
*/
getLocale (vm) {
if (!this.selectedLocale) {
this.selectedLocale = [
this.options.locale,
this.i18n(vm),
'en'
].reduce((selection, locale) => {
if (selection) {
return selection
}
if (locale) {
const option = parseLocale(locale)
.find(locale => has(this.options.locales, locale))
if (option) {
selection = option
}
}
return selection
}, false)
}
return this.selectedLocale
}
/**
* Get the validation message for a particular error.
*/
validationMessage (rule, validationContext, vm) {
const generators = this.options.locales[this.getLocale(vm)]
if (generators.hasOwnProperty(rule)) {
return generators[rule](validationContext)
} else if (rule[0] === '_' && generators.hasOwnProperty(rule.substr(1))) {
return generators[rule.substr(1)](validationContext)
}
if (generators.hasOwnProperty('default')) {
return generators.default(validationContext)
}
return 'This field does not have a valid value'
}
/**
* Given an instance of a FormulateForm register it.
* @param {vm} form
*/
register (form) {
if (form.$options.name === 'FormulateForm' && form.name) {
this.registry.set(form.name, form)
}
}
/**
* Given an instance of a form, remove it from the registry.
* @param {vm} form
*/
deregister (form) {
if (
form.$options.name === 'FormulateForm' &&
form.name &&
this.registry.has(form.name)
) {
this.registry.delete(form.name)
}
}
/**
* Given an array, this function will attempt to make sense of the given error
* and hydrate a form with the resulting errors.
*
* @param {error} err
* @param {string} formName
* @param {error}
*/
handle (err, formName, skip = false) {
const e = skip ? err : this.options.errorHandler(err, formName)
if (formName && this.registry.has(formName)) {
this.registry.get(formName).applyErrors({
formErrors: arrayify(e.formErrors),
inputErrors: e.inputErrors || {}
})
}
return e
}
/**
* Reset a form.
* @param {string} formName
* @param {object} initialValue
*/
reset (formName, initialValue = {}) {
this.resetValidation(formName)
this.setValues(formName, initialValue)
}
/**
* Reset the form's validation messages.
* @param {string} formName
*/
resetValidation (formName) {
const form = this.registry.get(formName)
form.hideErrors(formName)
form.namedErrors = []
form.namedFieldErrors = {}
}
/**
* Set the form values.
* @param {string} formName
* @param {object} values
*/
setValues (formName, values) {
if (values && !Array.isArray(values) && typeof values === 'object') {
const form = this.registry.get(formName)
form.setValues({ ...values })
}
}
/**
* Get the file uploader.
*/
getUploader () {
return this.options.uploader || false
}
/**
* Get the global upload url.
*/
getUploadUrl () {
return this.options.uploadUrl || false
}
/**
* When re-hydrating a file uploader with an array, get the sub-object key to
* access the url of the file. Usually this is just "url".
*/
getFileUrlKey () {
return this.options.fileUrlKey || 'url'
}
/**
* Create a new instance of an upload.
*/
createUpload (fileList, context) {
return new FileUpload(fileList, context, this.options)
}
}
export default new Formulate()

View File

@ -1,76 +0,0 @@
<template>
<ul
v-if="visibleErrors.length"
:class="`formulate-${type}-errors`"
>
<li
v-for="error in visibleErrors"
:key="error"
:class="`formulate-${type}-error`"
v-text="error"
/>
</ul>
</template>
<script>
import { arrayify } from './libs/utils'
export default {
inject: {
observeErrors: {
default: false
},
removeErrorObserver: {
default: false
}
},
props: {
context: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: 'form'
}
},
data () {
return {
boundSetErrors: this.setErrors.bind(this),
localErrors: []
}
},
computed: {
visibleValidationErrors () {
return Array.isArray(this.context.visibleValidationErrors) ? this.context.visibleValidationErrors : []
},
errors () {
return Array.isArray(this.context.errors) ? this.context.errors : []
},
mergedErrors () {
return this.errors.concat(this.localErrors)
},
visibleErrors () {
return Array.from(new Set(this.mergedErrors.concat(this.visibleValidationErrors)))
.filter(message => typeof message === 'string')
}
},
created () {
// This registration is for <FormulateErrors /> that are used for displaying
// Form errors in an override position.
if (this.type === 'form' && typeof this.observeErrors === 'function' && !Array.isArray(this.context.errors)) {
this.observeErrors({ callback: this.boundSetErrors, type: this.type })
}
},
destroyed () {
if (this.type === 'form' && typeof this.removeErrorObserver === 'function' && !Array.isArray(this.context.errors)) {
this.removeErrorObserver(this.boundSetErrors)
}
},
methods: {
setErrors (errors) {
this.localErrors = arrayify(errors)
}
}
}
</script>

View File

@ -1,85 +0,0 @@
<template>
<ul
v-if="fileUploads.length"
class="formulate-files"
>
<li
v-for="file in fileUploads"
:key="file.uuid"
:data-has-error="!!file.error"
:data-has-preview="!!(imagePreview && file.previewData)"
>
<div class="formulate-file">
<div
v-if="!!(imagePreview && file.previewData)"
class="formulate-file-image-preview"
>
<img
:src="file.previewData"
>
</div>
<div
class="formulate-file-name"
:title="file.name"
v-text="file.name"
/>
<div
v-if="file.progress !== false"
:data-just-finished="file.justFinished"
:data-is-finished="!file.justFinished && file.complete"
class="formulate-file-progress"
>
<div
class="formulate-file-progress-inner"
:style="{width: file.progress + '%'}"
/>
</div>
<div
v-if="(file.complete && !file.justFinished) || file.progress === false"
class="formulate-file-remove"
@click="file.removeFile"
/>
</div>
<div
v-if="file.error"
class="formulate-file-upload-error"
v-text="file.error"
/>
</li>
</ul>
</template>
<script>
import FileUpload from './FileUpload'
export default {
name: 'FormulateFiles',
props: {
files: {
type: FileUpload,
required: true
},
imagePreview: {
type: Boolean,
default: false
}
},
computed: {
fileUploads () {
return this.files.files || []
}
},
watch: {
files () {
if (this.imagePreview) {
this.files.loadPreviews()
}
}
},
mounted () {
if (this.imagePreview) {
this.files.loadPreviews()
}
}
}
</script>

View File

@ -1,175 +0,0 @@
<template>
<form
:class="classes"
@submit.prevent="formSubmitted"
>
<FormulateErrors
v-if="!hasFormErrorObservers"
:context="formContext"
/>
<slot />
</form>
</template>
<script>
import { arrayify, has } from './libs/utils'
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
import FormSubmission from './FormSubmission'
export default {
provide () {
return {
...useRegistryProviders(this),
observeErrors: this.addErrorObserver,
removeErrorObserver: this.removeErrorObserver,
formulateFieldValidation: this.formulateFieldValidation
}
},
name: 'FormulateForm',
model: {
prop: 'formulateValue',
event: 'input'
},
props: {
name: {
type: [String, Boolean],
default: false
},
formulateValue: {
type: Object,
default: () => ({})
},
values: {
type: [Object, Boolean],
default: false
},
errors: {
type: [Object, Boolean],
default: false
},
formErrors: {
type: Array,
default: () => ([])
}
},
data () {
return {
...useRegistry(this),
formShouldShowErrors: false,
errorObservers: [],
namedErrors: [],
namedFieldErrors: {}
}
},
computed: {
...useRegistryComputed(),
formContext () {
return {
errors: this.mergedFormErrors
}
},
classes () {
const classes = { 'formulate-form': true }
if (this.name) {
classes[`formulate-form--${this.name}`] = true
}
return classes
},
mergedFormErrors () {
return this.formErrors.concat(this.namedErrors)
},
mergedFieldErrors () {
const errors = {}
if (this.errors) {
for (const fieldName in this.errors) {
errors[fieldName] = arrayify(this.errors[fieldName])
}
}
for (const fieldName in this.namedFieldErrors) {
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
}
return errors
},
hasFormErrorObservers () {
return !!this.errorObservers.filter(o => o.type === 'form').length
}
},
watch: {
formulateValue: {
handler (values) {
if (this.isVmodeled &&
values &&
typeof values === 'object'
) {
this.setValues(values)
}
},
deep: true
},
mergedFormErrors (errors) {
this.errorObservers
.filter(o => o.type === 'form')
.forEach(o => o.callback(errors))
},
mergedFieldErrors: {
handler (errors) {
this.errorObservers
.filter(o => o.type === 'input')
.forEach(o => o.callback(errors[o.field] || []))
},
immediate: true
}
},
created () {
this.$formulate.register(this)
this.applyInitialValues()
},
destroyed () {
this.$formulate.deregister(this)
},
methods: {
...useRegistryMethods(),
applyErrors ({ formErrors, inputErrors }) {
// given an object of errors, apply them to this form
this.namedErrors = formErrors
this.namedFieldErrors = inputErrors
},
addErrorObserver (observer) {
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
this.errorObservers.push(observer)
if (observer.type === 'form') {
observer.callback(this.mergedFormErrors)
} else if (has(this.mergedFieldErrors, observer.field)) {
observer.callback(this.mergedFieldErrors[observer.field])
}
}
},
removeErrorObserver (observer) {
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
},
registerErrorComponent (component) {
if (!this.errorComponents.includes(component)) {
this.errorComponents.push(component)
}
},
formSubmitted () {
// perform validation here
this.showErrors()
const submission = new FormSubmission(this)
this.$emit('submit-raw', submission)
return submission.hasValidationErrors()
.then(hasErrors => hasErrors ? undefined : submission.values())
.then(data => {
if (typeof data !== 'undefined') {
this.$emit('submit', data)
return data
}
return undefined
})
},
formulateFieldValidation (errorObject) {
this.$emit('validation', errorObject)
}
}
}
</script>

View File

@ -1,122 +0,0 @@
<template>
<FormulateSlot
name="grouping"
class="formulate-input-grouping"
:context="context"
:force-wrap="context.repeatable"
>
<FormulateRepeatableProvider
v-for="(item, index) in items"
:key="item.__id"
:index="index"
:set-field-value="(field, value) => setFieldValue(index, field, value)"
:context="context"
@remove="removeItem"
>
<slot />
</FormulateRepeatableProvider>
</FormulateSlot>
</template>
<script>
import { setId } from './libs/utils'
export default {
name: 'FormulateGrouping',
props: {
context: {
type: Object,
required: true
}
},
provide () {
return {
isSubField: () => true,
registerProvider: this.registerProvider,
deregisterProvider: this.deregisterProvider
}
},
data () {
return {
providers: []
}
},
inject: ['formulateRegisterRule', 'formulateRemoveRule'],
computed: {
items () {
if (Array.isArray(this.context.model)) {
if (!this.context.repeatable && this.context.model.length === 0) {
return [setId({})]
}
return this.context.model.map(item => setId(item, item.__id))
}
return [setId({})]
},
formShouldShowErrors () {
return this.context.formShouldShowErrors
}
},
watch: {
providers () {
if (this.formShouldShowErrors) {
this.showErrors()
}
},
formShouldShowErrors (val) {
if (val) {
this.showErrors()
}
}
},
created () {
// We register with an error message of 'true' which causes the validation to fail but no message output.
this.formulateRegisterRule(this.validateGroup.bind(this), [], 'formulateGrouping', true)
},
destroyed () {
this.formulateRemoveRule('formulateGrouping')
},
methods: {
getAtIndex (index) {
if (typeof this.context.model[index] !== 'undefined' && this.context.model[index].__id) {
return this.context.model[index]
} else if (typeof this.context.model[index] !== 'undefined') {
return setId(this.context.model[index])
} else if (typeof this.context.model[index] === 'undefined' && typeof this.items[index] !== 'undefined') {
return setId({}, this.items[index].__id)
}
return setId({})
},
setFieldValue (index, field, value) {
const values = Array.isArray(this.context.model) ? this.context.model : []
const previous = this.getAtIndex(index)
const updated = setId(Object.assign({}, previous, { [field]: value }), previous.__id)
values.splice(index, 1, updated)
this.context.model = values
},
validateGroup () {
return Promise.all(this.providers.reduce((resolvers, provider) => {
if (provider && typeof provider.hasValidationErrors === 'function') {
resolvers.push(provider.hasValidationErrors())
}
return resolvers
}, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors))
},
showErrors () {
this.providers.forEach(p => p && typeof p.showErrors === 'function' && p.showErrors())
},
removeItem (index) {
if (Array.isArray(this.context.model)) {
this.context.model.splice(index, 1)
}
},
registerProvider (provider) {
if (!this.providers.some(p => p === provider)) {
this.providers.push(provider)
}
},
deregisterProvider (provider) {
this.providers = this.providers.filter(p => p !== provider)
}
}
}
</script>

View File

@ -1,396 +0,0 @@
<template>
<div
class="formulate-input"
:data-classification="classification"
:data-has-errors="hasErrors"
:data-is-showing-errors="hasVisibleErrors"
:data-type="type"
>
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
</div>
</template>
<script>
import context from './libs/context'
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
export default {
name: 'FormulateInput',
inheritAttrs: false,
provide () {
return {
// Allows sub-components of this input to register arbitrary rules.
formulateRegisterRule: this.registerRule,
formulateRemoveRule: this.removeRule
}
},
inject: {
formulateSetter: { default: undefined },
formulateFieldValidation: { default: () => () => ({}) },
formulateRegister: { default: undefined },
formulateDeregister: { default: undefined },
getFormValues: { default: () => () => ({}) },
observeErrors: { default: undefined },
removeErrorObserver: { default: undefined },
isSubField: { default: () => () => false }
},
model: {
prop: 'formulateValue',
event: 'input'
},
props: {
type: {
type: String,
default: 'text'
},
name: {
type: [String, Boolean],
default: true
},
/* eslint-disable */
formulateValue: {
default: ''
},
value: {
default: false
},
/* eslint-enable */
options: {
type: [Object, Array, Boolean],
default: false
},
optionGroups: {
type: [Object, Boolean],
default: false
},
id: {
type: [String, Boolean, Number],
default: false
},
label: {
type: [String, Boolean],
default: false
},
labelPosition: {
type: [String, Boolean],
default: false
},
limit: {
type: Number,
default: Infinity
},
help: {
type: [String, Boolean],
default: false
},
helpPosition: {
type: [String, Boolean],
default: false
},
errors: {
type: [String, Array, Boolean],
default: false
},
repeatable: {
type: Boolean,
default: false
},
validation: {
type: [String, Boolean, Array],
default: false
},
validationName: {
type: [String, Boolean],
default: false
},
error: {
type: [String, Boolean],
default: false
},
errorBehavior: {
type: String,
default: 'blur',
validator: function (value) {
return ['blur', 'live', 'submit'].includes(value)
}
},
showErrors: {
type: Boolean,
default: false
},
imageBehavior: {
type: String,
default: 'preview'
},
uploadUrl: {
type: [String, Boolean],
default: false
},
uploader: {
type: [Function, Object, Boolean],
default: false
},
uploadBehavior: {
type: String,
default: 'live'
},
preventWindowDrops: {
type: Boolean,
default: true
},
showValue: {
type: [String, Boolean],
default: false
},
validationMessages: {
type: Object,
default: () => ({})
},
validationRules: {
type: Object,
default: () => ({})
},
checked: {
type: [String, Boolean],
default: false
},
disableErrors: {
type: Boolean,
default: false
},
addLabel: {
type: [Boolean, String],
default: false
}
},
data () {
return {
defaultId: this.$formulate.nextId(this),
localAttributes: {},
localErrors: [],
proxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false,
validationErrors: [],
pendingValidation: Promise.resolve(),
// These registries are used for injected messages registrants only (mostly internal).
ruleRegistry: [],
messageRegistry: {}
}
},
computed: {
...context,
classification () {
const classification = this.$formulate.classify(this.type)
return (classification === 'box' && this.options) ? 'group' : classification
},
component () {
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
},
parsedValidationRules () {
const parsedValidationRules = {}
Object.keys(this.validationRules).forEach(key => {
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
})
return parsedValidationRules
},
messages () {
const messages = {}
Object.keys(this.validationMessages).forEach((key) => {
messages[snakeToCamel(key)] = this.validationMessages[key]
})
Object.keys(this.messageRegistry).forEach((key) => {
messages[snakeToCamel(key)] = this.messageRegistry[key]
})
return messages
}
},
watch: {
'$attrs': {
handler (value) {
this.updateLocalAttributes(value)
},
deep: true
},
proxy (newValue, oldValue) {
this.performValidation()
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
formulateValue (newValue, oldValue) {
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
showValidationErrors: {
handler (val) {
this.$emit('error-visibility', val)
},
immediate: true
}
},
created () {
this.applyInitialValue()
if (this.formulateRegister && typeof this.formulateRegister === 'function') {
this.formulateRegister(this.nameOrFallback, this)
}
if (!this.disableErrors && typeof this.observeErrors === 'function') {
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
}
this.updateLocalAttributes(this.$attrs)
this.performValidation()
},
beforeDestroy () {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
this.removeErrorObserver(this.setErrors)
}
if (typeof this.formulateDeregister === 'function') {
this.formulateDeregister(this.nameOrFallback)
}
},
methods: {
getInitialValue () {
// Manually request classification, pre-computed props
var classification = this.$formulate.classify(this.type)
classification = (classification === 'box' && this.options) ? 'group' : classification
if (classification === 'box' && this.checked) {
return this.value || true
} else if (has(this.$options.propsData, 'value') && classification !== 'box') {
return this.value
} else if (has(this.$options.propsData, 'formulateValue')) {
return this.formulateValue
}
return ''
},
applyInitialValue () {
// This should only be run immediately on created and ensures that the
// proxy and the model are both the same before any additional registration.
if (
!shallowEqualObjects(this.context.model, this.proxy) &&
// we dont' want to set the model if we are a sub-box of a multi-box field
(Object.prototype.hasOwnProperty(this.$options.propsData, 'options') && this.classification === 'box')
) {
this.context.model = this.proxy
}
},
updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value
}
},
performValidation () {
let rules = parseRules(this.validation, this.$formulate.rules(this.parsedValidationRules))
// Add in ruleRegistry rules. These are added directly via injection from
// children and not part of the standard validation rule set.
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
this.pendingValidation = this.runRules(rules)
.then(messages => this.didValidate(messages))
return this.pendingValidation
},
runRules (rules) {
const run = ([rule, args, ruleName, modifier]) => {
var res = rule({
value: this.context.model,
getFormValues: this.getFormValues.bind(this),
name: this.context.name
}, ...args)
res = (res instanceof Promise) ? res : Promise.resolve(res)
return res.then(result => result ? false : this.getMessage(ruleName, args))
}
return new Promise(resolve => {
const resolveGroups = (groups, allMessages = []) => {
const ruleGroup = groups.shift()
if (Array.isArray(ruleGroup) && ruleGroup.length) {
Promise.all(ruleGroup.map(run))
.then(messages => messages.filter(m => !!m))
.then(messages => {
messages = Array.isArray(messages) ? messages : []
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
if ((!messages.length || !ruleGroup.bail) && groups.length) {
return resolveGroups(groups, allMessages.concat(messages))
}
return resolve(allMessages.concat(messages))
})
} else {
resolve([])
}
}
resolveGroups(groupBails(rules))
})
},
didValidate (messages) {
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
this.validationErrors = messages
if (validationChanged) {
const errorObject = this.getErrorObject()
this.$emit('validation', errorObject)
if (this.formulateFieldValidation && typeof this.formulateFieldValidation === 'function') {
this.formulateFieldValidation(errorObject)
}
}
},
getMessage (ruleName, args) {
return this.getMessageFunc(ruleName)({
args,
name: this.mergedValidationName,
value: this.context.model,
vm: this,
formValues: this.getFormValues()
})
},
getMessageFunc (ruleName) {
ruleName = snakeToCamel(ruleName)
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
switch (typeof this.messages[ruleName]) {
case 'function':
return this.messages[ruleName]
case 'string':
case 'boolean':
return () => this.messages[ruleName]
}
}
return (context) => this.$formulate.validationMessage(ruleName, context, this)
},
hasValidationErrors () {
return new Promise(resolve => {
this.$nextTick(() => {
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
})
})
},
getValidationErrors () {
return new Promise(resolve => {
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
})
},
getErrorObject () {
return {
name: this.context.nameOrFallback || this.context.name,
errors: this.validationErrors.filter(s => typeof s === 'string'),
hasErrors: !!this.validationErrors.length
}
},
setErrors (errors) {
this.localErrors = arrayify(errors)
},
registerRule (rule, args, ruleName, message = null) {
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
// These are the raw rule format since they will be used directly.
this.ruleRegistry.push([rule, args, ruleName])
if (message !== null) {
this.messageRegistry[ruleName] = message
}
}
},
removeRule (key) {
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
if (ruleIndex >= 0) {
this.ruleRegistry.splice(ruleIndex, 1)
delete this.messageRegistry[key]
}
}
}
}
</script>

View File

@ -1,25 +0,0 @@
/**
* Default base for input components.
*/
export default {
props: {
context: {
type: Object,
required: true
}
},
computed: {
type () {
return this.context.type
},
id () {
return this.context.id
},
attributes () {
return this.context.attributes || {}
},
hasValue () {
return !!this.context.model
}
}
}

View File

@ -1,73 +0,0 @@
<template>
<FormulateSlot
name="repeatable"
:context="context"
:index="index"
:remove-item="removeItem"
>
<component
:is="context.slotComponents.repeatable"
:context="context"
:index="index"
:remove-item="removeItem"
>
<FormulateSlot
:context="context"
:index="index"
name="default"
/>
</component>
</FormulateSlot>
</template>
<script>
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
export default {
provide () {
return {
...useRegistryProviders(this),
formulateSetter: (field, value) => this.setFieldValue(field, value)
}
},
inject: {
registerProvider: 'registerProvider',
deregisterProvider: 'deregisterProvider'
},
props: {
index: {
type: Number,
required: true
},
context: {
type: Object,
required: true
},
setFieldValue: {
type: Function,
required: true
}
},
data () {
return {
...useRegistry(this),
isGrouping: true
}
},
computed: {
...useRegistryComputed()
},
created () {
this.registerProvider(this)
},
beforeDestroy () {
this.deregisterProvider(this)
},
methods: {
...useRegistryMethods(['setFieldValue']),
removeItem () {
this.$emit('remove', this.index)
}
}
}
</script>

View File

@ -1,36 +0,0 @@
export default {
inheritAttrs: false,
functional: true,
render (h, { props, data, parent, children }) {
var p = parent
var { name, forceWrap, context, ...mergeWithContext } = props
// Look up the ancestor tree for the first FormulateInput
while (p && p.$options.name !== 'FormulateInput') {
p = p.$parent
}
// if we never found the proper parent, just end it.
if (!p) {
return null
}
// If we found a formulate input, check for a matching scoped slot
if (p.$scopedSlots && p.$scopedSlots[props.name]) {
return p.$scopedSlots[props.name]({ ...context, ...mergeWithContext })
}
// If we found no scoped slot, take the children and render those inside a wrapper if there are multiple
if (Array.isArray(children) && (children.length > 1 || (forceWrap && children.length > 0))) {
const { name, context, ...attrs } = data.attrs
return h('div', { ...data, ...{ attrs } }, children)
// If there is only one child, render it alone
} else if (Array.isArray(children) && children.length === 1) {
return children[0]
}
// If there are no children, render nothing
return null
}
}

View File

@ -1,113 +0,0 @@
<template>
<div
class="formulate-input-group"
:data-is-repeatable="context.repeatable"
>
<template
v-if="subType !== 'grouping'"
>
<FormulateInput
v-for="optionContext in optionsWithContext"
:key="optionContext.id"
v-model="context.model"
v-bind="optionContext"
:disable-errors="true"
class="formulate-input-group-item"
@blur="context.blurHandler"
/>
</template>
<template
v-else
>
<FormulateGrouping
:context="context"
>
<slot />
</FormulateGrouping>
<FormulateSlot
v-if="canAddMore"
name="addmore"
:context="context"
:add-more="addItem"
>
<component
:is="context.slotComponents.addMore"
:context="context"
:add-more="addItem"
@add="addItem"
/>
</FormulateSlot>
</template>
</div>
</template>
<script>
import { setId } from '../libs/utils'
export default {
name: 'FormulateInputGroup',
props: {
context: {
type: Object,
required: true
}
},
computed: {
options () {
return this.context.options || []
},
subType () {
return (this.context.type === 'group') ? 'grouping' : 'inputs'
},
optionsWithContext () {
const {
// The following are a list of items to pull out of the context object
attributes: { id, ...groupApplicableAttributes },
blurHandler,
classification,
component,
getValidationErrors,
hasLabel,
hasValidationErrors,
isSubField,
labelPosition,
options,
performValidation,
setErrors,
slotComponents,
validationErrors,
visibleValidationErrors,
help,
...context
} = this.context
return this.options.map(option => this.groupItemContext(
context,
option,
groupApplicableAttributes
))
},
canAddMore () {
return (this.context.repeatable && this.items.length < this.context.limit)
},
items () {
return Array.isArray(this.context.model) ? this.context.model : [{}]
}
},
methods: {
addItem () {
if (Array.isArray(this.context.model)) {
this.context.model.push(setId({}))
return
}
this.context.model = this.items.concat([setId({})])
},
groupItemContext (context, option, groupAttributes) {
const optionAttributes = {}
const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes, !context.hasGivenName ? {
name: true
} : {})
return ctx
}
}
}
</script>

View File

@ -8,25 +8,18 @@ import { map, arrayify, shallowEqualObjects } from './utils'
export default {
context () {
return defineModel.call(this, {
addLabel: this.logicalAddLabel,
attributes: this.elementAttributes,
blurHandler: blurHandler.bind(this),
classification: this.classification,
component: this.component,
disableErrors: this.disableErrors,
errors: this.explicitErrors,
allErrors: this.allErrors,
formShouldShowErrors: this.formShouldShowErrors,
getValidationErrors: this.getValidationErrors.bind(this),
hasGivenName: this.hasGivenName,
hasLabel: (this.label && this.classification !== 'button'),
hasValidationErrors: this.hasValidationErrors.bind(this),
help: this.help,
helpPosition: this.logicalHelpPosition,
id: this.id || this.defaultId,
imageBehavior: this.imageBehavior,
label: this.label,
labelPosition: this.logicalLabelPosition,
limit: this.limit,
name: this.nameOrFallback,
performValidation: this.performValidation.bind(this),
@ -34,25 +27,18 @@ export default {
repeatable: this.repeatable,
setErrors: this.setErrors.bind(this),
showValidationErrors: this.showValidationErrors,
slotComponents: this.slotComponents,
type: this.type,
uploadBehavior: this.uploadBehavior,
uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulate.getUploader(),
uploader: this.uploader || this.$formulario.getUploader(),
validationErrors: this.validationErrors,
value: this.value,
visibleValidationErrors: this.visibleValidationErrors,
isSubField: this.isSubField,
...this.typeContext
})
},
// Used in sub-context
nameOrFallback,
hasGivenName,
typeContext,
elementAttributes,
logicalLabelPosition,
logicalHelpPosition,
mergedUploadUrl,
// These items are not passed as context
@ -63,44 +49,7 @@ export default {
hasErrors,
hasVisibleErrors,
showValidationErrors,
visibleValidationErrors,
slotComponents,
logicalAddLabel
}
/**
* The label to display when adding a new group.
*/
function logicalAddLabel () {
if (typeof this.addLabel === 'boolean') {
return `+ ${this.label || this.name || 'Add'}`
}
return this.addLabel
}
/**
* Given (this.type), return an object to merge with the context
* @return {object}
* @return {object}
*/
function typeContext () {
switch (this.classification) {
case 'select':
return {
options: createOptionList.call(this, this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, (k, v) => createOptionList.call(this, v)) : false,
placeholder: this.$attrs.placeholder || false
}
case 'slider':
return { showValue: !!this.showValue }
default:
if (this.options) {
return {
options: createOptionList.call(this, this.options)
}
}
return {}
}
visibleValidationErrors
}
/**
@ -128,37 +77,6 @@ function elementAttributes () {
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.
*/
@ -166,13 +84,8 @@ function mergedValidationName () {
if (this.validationName) {
return this.validationName
}
if (typeof this.name === 'string') {
return this.name
}
if (this.label) {
return this.label
}
return this.type
}
/**
@ -180,7 +93,7 @@ function mergedValidationName () {
* that is defined as a plugin option.
*/
function mergedUploadUrl () {
return this.uploadUrl || this.$formulate.getUploadUrl()
return this.uploadUrl || this.$formulario.getUploadUrl()
}
/**
@ -191,9 +104,7 @@ function showValidationErrors () {
if (this.showErrors || this.formShouldShowErrors) {
return true
}
if (this.classification === 'file' && this.uploadBehavior === 'live' && modelGetter.call(this)) {
return true
}
return this.behavioralErrorVisibility
}
@ -209,12 +120,10 @@ function visibleValidationErrors () {
* Return the elements name, or select a fallback.
*/
function nameOrFallback () {
if (this.name === true && this.classification !== 'button') {
return `${this.type}_${this.elementAttributes.id}`
}
if (this.name === false || (this.classification === 'button' && this.name === true)) {
return false
if (this.path !== '') {
return this.path + '.' + 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 () {
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
@ -284,20 +193,6 @@ function hasVisibleErrors () {
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
}
/**
* The component that should be rendered in the label slot as default.
*/
function slotComponents () {
return {
label: this.$formulate.slotComponent(this.type, 'label'),
help: this.$formulate.slotComponent(this.type, 'help'),
errors: this.$formulate.slotComponent(this.type, 'errors'),
repeatable: this.$formulate.slotComponent(this.type, 'repeatable'),
addMore: this.$formulate.slotComponent(this.type, 'addMore'),
remove: this.$formulate.slotComponent(this.type, 'remove')
}
}
/**
* Bound into the context object.
*/
@ -323,11 +218,8 @@ function defineModel (context) {
* Get the value from a model.
**/
function modelGetter () {
const model = this.isVmodeled ? 'formulateValue' : 'proxy'
if (this.type === 'checkbox' && !Array.isArray(this[model]) && this.options) {
return []
}
if (!this[model]) {
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
if (this[model] === undefined) {
return ''
}
return this[model]
@ -341,7 +233,7 @@ function modelSetter (value) {
this.proxy = value
}
this.$emit('input', value)
if (this.context.name && typeof this.formulateSetter === 'function') {
this.formulateSetter(this.context.name, value)
if (this.context.name && typeof this.formularioSetter === 'function') {
this.formularioSetter(this.context.name, value)
}
}

View File

@ -4,7 +4,7 @@
* Note: We're shipping front end code here, file size is critical. This file is
* overly terse for that reason alone, we wouldn't necessarily recommend this.
*/
const fi = 'FormulateInput'
const fi = 'FormularioInput'
const add = (n, c) => ({
classification: n,
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))

View File

@ -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
@ -78,19 +78,19 @@ class Registry {
return false
}
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')
if (
!hasVModelValue &&
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
// element is not, set it directly.
component.context.model = this.ctx.initialValues[field]
component.context.model = getNested(this.ctx.initialValues, field)
} else if (
(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
// form has no value or a different value, so use the field value
@ -142,24 +142,24 @@ export function useRegistryComputed () {
return {
hasInitialValue () {
return (
(this.formulateValue && typeof this.formulateValue === 'object') ||
(this.formularioValue && typeof this.formularioValue === 'object') ||
(this.values && typeof this.values === 'object') ||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
)
},
isVmodeled () {
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
},
initialValues () {
if (
has(this.$options.propsData, 'formulateValue') &&
typeof this.formulateValue === 'object'
has(this.$options.propsData, 'formularioValue') &&
typeof this.formularioValue === 'object'
) {
// If there is a v-model on the form/group, use those values as first priority
return Object.assign({}, this.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 (
has(this.$options.propsData, 'values') &&
typeof this.values === 'object'
@ -191,7 +191,7 @@ export function useRegistryMethods (without = []) {
const { [field]: value, ...proxy } = this.proxy
this.proxy = proxy
} else {
Object.assign(this.proxy, { [field]: value })
setNested(this.proxy, field, value);
}
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))))
keys.forEach(field => {
if (this.registry.has(field) &&
!shallowEqualObjects(values[field], this.proxy[field]) &&
!shallowEqualObjects(values[field], this.registry.get(field).proxy)
!shallowEqualObjects(getNested(values, field), getNested(this.proxy, field)) &&
!shallowEqualObjects(getNested(values, field), this.registry.get(field).proxy)
) {
this.setFieldValue(field, values[field])
this.registry.get(field).context.model = values[field]
this.setFieldValue(field, getNested(values, field))
this.registry.get(field).context.model = getNested(values, field)
}
})
}
@ -241,9 +241,9 @@ export function useRegistryMethods (without = []) {
*/
export function useRegistryProviders (ctx) {
return {
formulateSetter: ctx.setFieldValue,
formulateRegister: ctx.register,
formulateDeregister: ctx.deregister,
formularioSetter: ctx.setFieldValue,
formularioRegister: ctx.register,
formularioDeregister: ctx.deregister,
getFormValues: ctx.getFormValues
}
}

View File

@ -284,3 +284,63 @@ export function has (ctx, prop) {
export function setId (o, id) {
return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') }))
}
export function getNested(obj, field) {
let fieldParts = field.split('.');
let result = obj;
for (const key in fieldParts) {
let matches = fieldParts[key].match(/(.+)\[(\d+)\]$/);
if (result === undefined) {
return null
}
if (matches) {
result = result[matches[1]];
if (result === undefined) {
return null
}
result = result[matches[2]];
} else {
result = result[fieldParts[key]];
}
}
return result;
}
export function setNested(obj, field, value) {
let fieldParts = field.split('.');
let subProxy = obj;
for (let i = 0; i < fieldParts.length; i++) {
let fieldPart = fieldParts[i];
let matches = fieldPart.match(/(.+)\[(\d+)\]$/);
if (matches) {
if (subProxy[matches[1]] === undefined) {
subProxy[matches[1]] = [];
}
subProxy = subProxy[matches[1]];
if (i == fieldParts.length - 1) {
subProxy[matches[2]] = value
break;
} else {
subProxy = subProxy[matches[2]];
}
} else {
if (i == fieldParts.length - 1) {
subProxy[fieldPart] = value
break;
} else {
if (subProxy[fieldPart] === undefined) {
subProxy[fieldPart] = {};
}
subProxy = subProxy[fieldPart];
}
}
}
return obj;
}

View File

@ -1,9 +0,0 @@
# i18n moved
Locales have been removed from vue-formulate core to [vue-formulate-i18n](https://github.com/wearebraid/vue-formulate-i18n).
This was done to allow for better tree-shaking by bundlers and allow
for lots of additional language support without increasing the size of the core package.
## PRs welcome
[Please read the i18n contribution documentation](https://www.vueformulate.com/guide/contributing/#internationalization).

View File

@ -1,26 +0,0 @@
<template>
<div class="formulate-input-group-add-more">
<FormulateInput
type="button"
:label="context.addLabel"
data-minor
data-ghost
@click="addMore"
/>
</div>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
},
addMore: {
type: Function,
required: true
}
}
}
</script>

View File

@ -1,19 +0,0 @@
<template>
<div
v-if="context.help"
:id="`${context.id}-help`"
:class="`formulate-input-help formulate-input-help--${context.helpPosition}`"
v-text="context.help"
/>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
}
}
}
</script>

View File

@ -1,18 +0,0 @@
<template>
<label
:class="`formulate-input-label formulate-input-label--${context.labelPosition}`"
:for="context.id"
v-text="context.label"
/>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
}
}
}
</script>

View File

@ -1,37 +0,0 @@
<template>
<div
class="formulate-input-group-repeatable"
>
<FormulateSlot
name="remove"
:context="context"
:remove-item="removeItem"
>
<component
:is="context.slotComponents.remove"
:context="context"
:remove-item="removeItem"
/>
</FormulateSlot>
<slot />
</div>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
},
removeItem: {
type: Function,
required: true
},
index: {
type: Number,
required: true
}
}
}
</script>

View File

@ -1,25 +0,0 @@
<template>
<a
v-if="context.repeatable"
class="formulate-input-group-repeatable-remove"
role="button"
@click.prevent="removeItem"
@keypress.enter="removeItem"
v-text="`Remove`"
/>
</template>
<script>
export default {
props: {
context: {
type: Object,
required: true
},
removeItem: {
type: Function,
required: true
}
}
}
</script>