1
0
mirror of synced 2024-11-25 14:56:03 +03:00

Merge branch 'release/2.3.0' of github.com:wearebraid/vue-formulate into release/2.3.0

This commit is contained in:
Justin Schroeder 2020-03-28 23:36:07 -04:00
commit 72552cd84d
15 changed files with 249 additions and 54 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
dist/snow.css vendored
View File

@ -1,5 +1,6 @@
.formulate-input {
margin-bottom: 1.5em; }
margin-bottom: 1.5em;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; }
.formulate-input .formulate-input-label {
display: block;
line-height: 1.5;

2
dist/snow.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -150,8 +150,13 @@ class Formulate {
* Attempt to get the vue-i18n configured locale.
*/
i18n (vm) {
if (vm.$i18n && vm.$i18n.locale) {
if (vm.$i18n) {
switch (typeof vm.$i18n.locale) {
case 'string':
return vm.$i18n.locale
case 'function':
return vm.$i18n.locale()
}
}
return false
}

View File

@ -25,29 +25,13 @@ export default {
}
},
props: {
showValidationErrors: {
type: Boolean,
default: false
},
errors: {
type: [Array, Boolean],
default: false
},
validationErrors: {
type: [Array],
default: () => ([])
context: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: 'form'
},
preventRegistration: {
type: Boolean,
default: false
},
fieldName: {
type: [String, Boolean],
default: false
}
},
data () {
@ -57,20 +41,28 @@ export default {
}
},
computed: {
visibleValidationErrors () {
return Array.isArray(this.context.visibleValidationErrors) ? this.context.visibleValidationErrors : []
},
errors () {
return Array.isArray(this.context.errors) ? this.context.errors : []
},
mergedErrors () {
return arrayify(this.errors).concat(this.localErrors)
return this.errors.concat(this.localErrors)
},
visibleErrors () {
return Array.from(new Set(this.mergedErrors.concat(this.showValidationErrors ? this.validationErrors : [])))
return Array.from(new Set(this.mergedErrors.concat(this.visibleValidationErrors)))
}
},
created () {
if (!this.preventRegistration && typeof this.observeErrors === 'function' && (this.type === 'form' || this.fieldName)) {
this.observeErrors({ callback: this.boundSetErrors, type: this.type, field: this.fieldName })
// 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.preventRegistration && typeof this.removeErrorObserver === 'function') {
if (this.type === 'form' && typeof this.removeErrorObserver === 'function' && !Array.isArray(this.context.errors)) {
this.removeErrorObserver(this.boundSetErrors)
}
},

View File

@ -5,9 +5,7 @@
>
<FormulateErrors
v-if="!hasFormErrorObservers"
type="form"
:errors="mergedFormErrors"
:prevent-registration="true"
:context="formContext"
/>
<slot />
</form>
@ -24,7 +22,8 @@ export default {
formulateFormRegister: this.register,
getFormValues: this.getFormValues,
observeErrors: this.addErrorObserver,
removeErrorObserver: this.removeErrorObserver
removeErrorObserver: this.removeErrorObserver,
formulateFieldValidation: this.formulateFieldValidation
}
},
name: 'FormulateForm',
@ -65,6 +64,15 @@ export default {
}
},
computed: {
/**
* @todo in 2.3.0 this will expand and be extracted to a separate module to
* support better scoped slot interoperability.
*/
formContext () {
return {
errors: this.mergedFormErrors
}
},
hasInitialValue () {
return (
(this.formulateValue && typeof this.formulateValue === 'object') ||
@ -245,14 +253,19 @@ export default {
getFormValues () {
return this.internalFormModelProxy
},
formulateFieldValidation (errorObject) {
this.$emit('validation', errorObject)
},
hasValidationErrors () {
const resolvers = []
for (const fieldName in this.registry) {
if (typeof this.registry[fieldName].hasValidationErrors === 'function') {
resolvers.push(this.registry[fieldName].hasValidationErrors())
if (typeof this.registry[fieldName].getValidationErrors === 'function') {
resolvers.push(this.registry[fieldName].getValidationErrors())
}
}
return Promise.all(resolvers).then(fields => !!fields.find(hasErrors => hasErrors))
return Promise.all(resolvers).then((errorObjects) => {
return errorObjects.some(item => item.hasErrors)
})
}
}
}

View File

@ -49,17 +49,14 @@
<FormulateErrors
v-if="!disableErrors"
:type="`input`"
:errors="explicitErrors"
:field-name="nameOrFallback"
:validation-errors="validationErrors"
:show-validation-errors="showValidationErrors"
:context="context"
/>
</div>
</template>
<script>
import context from './libs/context'
import { shallowEqualObjects, parseRules, snakeToCamel } from './libs/utils'
import { shallowEqualObjects, parseRules, snakeToCamel, arrayify } from './libs/utils'
import nanoid from 'nanoid/non-secure'
export default {
@ -67,8 +64,11 @@ export default {
inheritAttrs: false,
inject: {
formulateFormSetter: { default: undefined },
formulateFieldValidation: { default: () => () => ({}) },
formulateFormRegister: { default: undefined },
getFormValues: { default: () => () => ({}) }
getFormValues: { default: () => () => ({}) },
observeErrors: { default: undefined },
removeErrorObserver: { default: undefined }
},
model: {
prop: 'formulateValue',
@ -191,6 +191,7 @@ export default {
return {
defaultId: nanoid(9),
localAttributes: {},
localErrors: [],
internalModelProxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false,
@ -239,6 +240,12 @@ export default {
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue
}
},
showValidationErrors: {
handler (val) {
this.$emit('error-visibility', val)
},
immediate: true
}
},
created () {
@ -246,9 +253,17 @@ export default {
if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') {
this.formulateFormRegister(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()
},
destroyed () {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
this.removeErrorObserver(this.setErrors)
}
},
methods: {
getInitialValue () {
// Manually request classification, pre-computed props
@ -293,9 +308,20 @@ export default {
})
)
.then(result => result.filter(result => result))
.then(errorMessages => { this.validationErrors = errorMessages })
.then(messages => this.didValidate(messages))
return this.pendingValidation
},
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,
@ -323,6 +349,23 @@ export default {
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,
hasErrors: !!this.validationErrors.length
}
},
setErrors (errors) {
this.localErrors = arrayify(errors)
}
}
}

View File

@ -40,6 +40,10 @@ export default {
blurHandler,
performValidation,
hasValidationErrors,
getValidationErrors,
validationErrors,
setErrors,
visibleValidationErrors,
component,
hasLabel,
...context

View File

@ -27,6 +27,12 @@ export default {
uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops,
hasValidationErrors: this.hasValidationErrors,
getValidationErrors: this.getValidationErrors.bind(this),
validationErrors: this.validationErrors,
errors: this.explicitErrors,
setErrors: this.setErrors.bind(this),
showValidationErrors: this.showValidationErrors,
visibleValidationErrors: this.visibleValidationErrors,
...this.typeContext
})
},
@ -44,7 +50,8 @@ export default {
allErrors,
hasErrors,
hasVisibleErrors,
showValidationErrors
showValidationErrors,
visibleValidationErrors
}
/**
@ -134,12 +141,20 @@ function showValidationErrors () {
if (this.showErrors || this.formShouldShowErrors) {
return true
}
if (this.classification === 'file' && this.uploadBehavior === 'live' && this.context.model) {
if (this.classification === 'file' && this.uploadBehavior === 'live' && modelGetter.call(this)) {
return true
}
return this.behavioralErrorVisibility
}
/**
* All of the currently visible validation errors (does not include error handling)
* @return {array}
*/
function visibleValidationErrors () {
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
}
/**
* Return the elements name, or select a fallback.
*/
@ -188,6 +203,7 @@ function createOptionList (options) {
*/
function explicitErrors () {
return arrayify(this.errors)
.concat(this.localErrors)
.concat(arrayify(this.error))
}
@ -207,7 +223,7 @@ function hasErrors () {
}
/**
* Checks if form has actively visible errors.
* Returns if form has actively visible errors (of any kind)
*/
function hasVisibleErrors () {
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)

View File

@ -153,7 +153,7 @@ describe('Formulate', () => {
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('nl-BE')
})
it('can select a matching locale using i18n', () => {
it('can select a matching locale using i18n locale string', () => {
Formulate.selectedLocale = false // reset the memoization
function Vue () {}
Vue.component = function (name, instance) {}
@ -166,4 +166,19 @@ describe('Formulate', () => {
})
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('cn')
})
it('can select a matching locale using i18n locale function', () => {
Formulate.selectedLocale = false // reset the memoization
function Vue () {}
Vue.component = function (name, instance) {}
const vm = { $i18n: {locale: () => 'en-US' } }
Formulate.install(Vue, {
locales: {
cn: {},
em: {},
en: {}
}
})
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('en')
})
})

View File

@ -23,7 +23,6 @@ describe('FormulateForm', () => {
expect(wrapper.find('form div.default-slot-item').exists()).toBe(true)
})
it('intercepts submit event', () => {
const formSubmitted = jest.fn()
const wrapper = mount(FormulateForm, {
@ -389,4 +388,50 @@ describe('FormulateForm', () => {
await flushPromises()
expect(wrapper.find(FormulateForm).vm.errorObservers.length).toBe(0)
})
it('emits correct validation event on entry', async () => {
const wrapper = mount(FormulateForm, {
slots: { default: `
<div>
<FormulateInput type="text" validation="required|in:bar" name="testinput" />
<FormulateInput type="radio" validation="required" name="bar" />
</div>
` }
})
wrapper.find('input[type="text"]').setValue('foo')
await flushPromises()
const errorObjects = wrapper.emitted('validation')
// There should be 3 events, both inputs mounting, and the value being set removing required on testinput
expect(errorObjects.length).toBe(3)
// this should be the event from the setValue()
const errorObject = errorObjects[2][0]
expect(errorObject).toEqual({
name: 'testinput',
errors: [
expect.any(String)
],
hasErrors: true
})
})
it('emits correct validation event when no errors', async () => {
const wrapper = mount(FormulateForm, {
slots: { default: `
<div>
<FormulateInput type="text" validation="required|in:bar" name="testinput" />
<FormulateInput type="radio" validation="required" name="bar" />
</div>
` }
})
wrapper.find('input[type="text"]').setValue('bar')
await flushPromises()
const errorObjects = wrapper.emitted('validation')
expect(errorObjects.length).toBe(3)
const errorObject = errorObjects[2][0]
expect(errorObject).toEqual({
name: 'testinput',
errors: [],
hasErrors: false
})
})
})

View File

@ -70,7 +70,6 @@ describe('FormulateInput', () => {
validationRules: {
foobar: async ({ value }) => value === 'foo'
},
validation: 'required|foobar',
errorBehavior: 'live',
value: 'bar'
} })
@ -88,7 +87,6 @@ describe('FormulateInput', () => {
validationRules: {
foobar: ({ value }) => value === 'foo'
},
validation: 'required|foobar',
errorBehavior: 'live',
value: 'bar'
} })
@ -117,4 +115,66 @@ describe('FormulateInput', () => {
await flushPromises()
expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
it('emits correct validation event', async () => {
const wrapper = mount(FormulateInput, { propsData: {
type: 'text',
validation: 'required',
errorBehavior: 'live',
value: '',
name: 'testinput',
} })
await flushPromises()
const errorObject = wrapper.emitted('validation')[0][0]
expect(errorObject).toEqual({
name: 'testinput',
errors: [
expect.any(String)
],
hasErrors: true
})
})
it('emits a error-visibility event on blur', async () => {
const wrapper = mount(FormulateInput, { propsData: {
type: 'text',
validation: 'required',
errorBehavior: 'blur',
value: '',
name: 'testinput',
} })
await flushPromises()
expect(wrapper.emitted('error-visibility')[0][0]).toBe(false)
wrapper.find('input[type="text"]').trigger('blur')
await flushPromises()
expect(wrapper.emitted('error-visibility')[1][0]).toBe(true)
})
it('emits error-visibility event immediately when live', async () => {
const wrapper = mount(FormulateInput, { propsData: {
type: 'text',
validation: 'required',
errorBehavior: 'live',
value: '',
name: 'testinput',
} })
await flushPromises()
expect(wrapper.emitted('error-visibility').length).toBe(1)
})
it('Does not emit an error-visibility event if visibility did not change', async () => {
const wrapper = mount(FormulateInput, { propsData: {
type: 'text',
validation: 'in:xyz',
errorBehavior: 'live',
value: 'bar',
name: 'testinput',
} })
await flushPromises()
expect(wrapper.emitted('error-visibility').length).toBe(1)
wrapper.find('input[type="text"]').setValue('bar')
await flushPromises()
expect(wrapper.emitted('error-visibility').length).toBe(1)
})
})

View File

@ -3,6 +3,7 @@
.formulate-input {
margin-bottom: 1.5em;
font-family: $formulate-font-stack;
.formulate-input-label {
display: block;