1
0
mirror of synced 2025-03-03 19:43:19 +03:00

Fixes issue with grouped options hydrating initial values

This commit is contained in:
Justin Schroeder 2020-03-01 22:29:54 -05:00
parent 1806671194
commit cf9396e0d6
10 changed files with 271 additions and 94 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

View File

@ -92,16 +92,18 @@ export default {
} }
} }
}, },
deep: true, deep: true
immediate: false
} }
}, },
created () { created () {
if (this.hasInitialValue) { this.applyInitialValues()
this.internalFormModelProxy = this.initialValues
}
}, },
methods: { methods: {
applyInitialValues () {
if (this.hasInitialValue) {
this.internalFormModelProxy = this.initialValues
}
},
setFieldValue (field, value) { setFieldValue (field, value) {
Object.assign(this.internalFormModelProxy, { [field]: value }) Object.assign(this.internalFormModelProxy, { [field]: value })
this.$emit('input', Object.assign({}, this.internalFormModelProxy)) this.$emit('input', Object.assign({}, this.internalFormModelProxy))

View File

@ -173,16 +173,17 @@ export default {
validationRules: { validationRules: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
checked: {
type: [String, Boolean],
default: false
} }
}, },
data () { data () {
return { return {
/**
* @todo consider swapping out nanoid for this._uid
*/
defaultId: nanoid(9), defaultId: nanoid(9),
localAttributes: {}, localAttributes: {},
internalModelProxy: this.formulateValue || this.value, internalModelProxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'), behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false, formShouldShowErrors: false,
validationErrors: [], validationErrors: [],
@ -219,6 +220,7 @@ export default {
} }
}, },
created () { created () {
this.applyInitialValue()
if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') { if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') {
this.formulateFormRegister(this.nameOrFallback, this) this.formulateFormRegister(this.nameOrFallback, this)
} }
@ -226,6 +228,30 @@ export default {
this.performValidation() this.performValidation()
}, },
methods: { 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 (Object.prototype.hasOwnProperty.call(this.$options.propsData, 'value') && classification !== 'box') {
return this.value
} else if (Object.prototype.hasOwnProperty.call(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.internalModelProxy) &&
// 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.internalModelProxy
}
},
updateLocalAttributes (value) { updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) { if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value this.localAttributes = value

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="formulate-input-group"> <div class="formulate-input-group">
<component <component
:is="optionContext.component" :is="subComponent"
v-for="optionContext in optionsWithContext" v-for="optionContext in optionsWithContext"
:key="optionContext.id" :key="optionContext.id"
v-model="context.model" v-model="context.model"
@ -24,16 +24,35 @@ export default {
options () { options () {
return this.context.options || [] return this.context.options || []
}, },
subComponent () {
// @todo - rough-in for future flexible input-groups
return 'FormulateInput'
},
optionsWithContext () { optionsWithContext () {
const { options, labelPosition, attributes, classification, ...context } = this.context const {
return this.options.map(option => this.groupItemContext(context, option)) // The following are a list of items to pull out of the context object
options,
labelPosition,
attributes: { id, ...groupApplicableAttributes },
classification,
blurHandler,
hasValidationErrors,
component,
hasLabel,
...context
} = this.context
return this.options.map(option => this.groupItemContext(
context,
option,
groupApplicableAttributes
))
} }
}, },
methods: { methods: {
groupItemContext (...args) { groupItemContext (context, option, groupAttributes) {
return Object.assign({}, ...args, { const optionAttributes = {}
component: 'FormulateInput' const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes)
}) return ctx
} }
} }
} }

View File

@ -1,5 +1,5 @@
import nanoid from 'nanoid/non-secure' import nanoid from 'nanoid/non-secure'
import { map, arrayify } from './utils' import { map, arrayify, shallowEqualObjects } from './utils'
/** /**
* For a single instance of an input, export all of the context needed to fully * For a single instance of an input, export all of the context needed to fully
@ -220,7 +220,9 @@ function modelGetter () {
* Set the value from a model. * Set the value from a model.
**/ **/
function modelSetter (value) { function modelSetter (value) {
this.internalModelProxy = value if (!shallowEqualObjects(value, this.internalModelProxy)) {
this.internalModelProxy = value
}
this.$emit('input', value) this.$emit('input', value)
if (this.context.name && typeof this.formulateFormSetter === 'function') { if (this.context.name && typeof this.formulateFormSetter === 'function') {
this.formulateFormSetter(this.context.name, value) this.formulateFormSetter(this.context.name, value)

View File

@ -46,11 +46,12 @@ describe('FormulateForm', () => {
expect(Object.keys(wrapper.vm.registry)).toEqual(['subinput1', 'subinput2']) expect(Object.keys(wrapper.vm.registry)).toEqual(['subinput1', 'subinput2'])
}) })
it('can set a fields initial value', () => { it('can set a fields initial value', async () => {
const wrapper = mount(FormulateForm, { const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { testinput: 'has initial value' } }, propsData: { formulateValue: { testinput: 'has initial value' } },
slots: { default: '<FormulateInput type="text" name="testinput" />' } slots: { default: '<FormulateInput type="text" name="testinput" />' }
}) })
await flushPromises()
expect(wrapper.find('input').element.value).toBe('has initial value') expect(wrapper.find('input').element.value).toBe('has initial value')
}) })
@ -76,6 +77,31 @@ describe('FormulateForm', () => {
expect(wrapper.vm.formValues).toEqual({ name: '123' }) expect(wrapper.vm.formValues).toEqual({ name: '123' })
}) })
it('can set initial checked attribute on single checkboxes', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box1: true } },
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
})
expect(wrapper.find('input[type="checkbox"]').is(':checked')).toBe(true)
});
it('can set initial unchecked attribute on single checkboxes', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box1: false } },
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
})
expect(wrapper.find('input[type="checkbox"]').is(':checked')).toBe(false)
});
it('can set checkbox initial value with options', async () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box2: ['second', 'third'] } },
slots: { default: '<FormulateInput type="checkbox" name="box2" :options="{first: \'First\', second: \'Second\', third: \'Third\'}" />' }
})
await flushPromises()
expect(wrapper.findAll('input').length).toBe(3)
});
it('receives updates to form model when individual fields are edited', () => { it('receives updates to form model when individual fields are edited', () => {
const wrapper = mount({ const wrapper = mount({
data () { data () {

View File

@ -8,75 +8,177 @@ import FormulateInputGroup from '@/FormulateInputGroup.vue'
Vue.use(Formulate) Vue.use(Formulate)
test('type "checkbox" renders a box element', () => { describe('FormulateInputBox', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } }) it('renders a box element when type "checkbox" ', () => {
expect(wrapper.contains(FormulateInputBox)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
}) expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
test('type "radio" renders a box element', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } }) it('renders a box element when type "radio"', () => {
expect(wrapper.contains(FormulateInputBox)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
}) expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
test('box inputs properly process options object in context library', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } }) it('box inputs properly process options object in context library', () => {
expect(Array.isArray(wrapper.vm.context.options)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
}) expect(Array.isArray(wrapper.vm.context.options)).toBe(true)
})
test('type "checkbox" with options renders a group', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } }) it('renders a group when type "checkbox" with options', () => {
expect(wrapper.contains(FormulateInputGroup)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
}) expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
test('type "radio" with options renders a group', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } }) it('renders a group when type "radio" with options', () => {
expect(wrapper.contains(FormulateInputGroup)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
}) expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
test('labelPosition of type "checkbox" defaults to after', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } }) it('defaults labelPosition to "after" when type "checkbox"', () => {
expect(wrapper.vm.context.labelPosition).toBe('after') const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
}) expect(wrapper.vm.context.labelPosition).toBe('after')
})
test('labelPosition of type "checkbox" with options defaults to before', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}}) it('labelPosition of defaults to before when type "checkbox" with options', () => {
expect(wrapper.vm.context.labelPosition).toBe('before') const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}})
}) expect(wrapper.vm.context.labelPosition).toBe('before')
})
test('type radio renders multiple inputs with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } }) it('renders multiple inputs with options when type "radio"', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(2) const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
}) expect(wrapper.findAll('input[type="radio"]').length).toBe(2)
})
test('type "radio" auto generate ids if not provided', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } }) it('generates ids if not provided when type "radio"', () => {
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
}) expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
/**
* Test data binding it('additional context does not bleed through to attributes with type "radio" and options', () => {
*/ const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(Object.keys(wrapper.find('input[type="radio"]').attributes())).toEqual(["type", "id", "value"])
test('type "checkbox" sets array of values via v-model', async () => { })
const wrapper = mount({
data () { it('additional context does not bleed through to attributes with type "checkbox" and options', () => {
return { const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
checkboxValues: [], expect(Object.keys(wrapper.find('input[type="checkbox"]').attributes())).toEqual(["type", "id", "value"])
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'} })
}
}, it('allows external attributes to make it down to the inner box elements', () => {
template: ` const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'}, readonly: 'true' } })
<div> expect(Object.keys(wrapper.find('input[type="radio"]').attributes()).includes('readonly')).toBe(true)
<FormulateInput type="checkbox" v-model="checkboxValues" :options="options" /> })
</div>
` it('does not use the value attribute to be checked', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', value: '123' } })
expect(wrapper.find('input').is(':checked')).toBe(false)
})
it('uses the checked attribute to be checked', async () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', checked: 'true' } })
await flushPromises()
await wrapper.vm.$nextTick()
expect(wrapper.find('input').is(':checked')).toBe(true)
})
it('uses the value attribute to select "type" radio when using options', async () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'}, value: 'b' } })
await flushPromises()
expect(wrapper.findAll('input:checked').length).toBe(1)
})
it('uses the value attribute to select "type" checkbox when using options', async () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2', c: '123'}, value: ['b', 'c'] } })
await flushPromises()
expect(wrapper.findAll('input:checked').length).toBe(2)
})
/**
* it data binding
*/
it('sets array of values via v-model when type "checkbox"', async () => {
const wrapper = mount({
data () {
return {
checkboxValues: [],
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}
}
},
template: `
<div>
<FormulateInput type="checkbox" v-model="checkboxValues" :options="options" />
</div>
`
})
const fooInputs = wrapper.findAll('input[value^="foo"]')
expect(fooInputs.length).toBe(2)
fooInputs.at(0).setChecked()
await flushPromises()
fooInputs.at(1).setChecked()
await flushPromises()
expect(wrapper.vm.checkboxValues).toEqual(['foo', 'fooey'])
})
it('does not pre-set internal value when type "radio" with options', async () => {
const wrapper = mount({
data () {
return {
radioValue: '',
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}
}
},
template: `
<div>
<FormulateInput type="radio" v-model="radioValue" :options="options" />
</div>
`
})
await wrapper.vm.$nextTick()
await flushPromises()
expect(wrapper.vm.radioValue).toBe('')
})
// it('does not pre-set internal value of FormulateForm when type "radio" with options', async () => {
// const wrapper = mount({
// data () {
// return {
// radioValue: '',
// formValues: {},
// options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}
// }
// },
// template: `
// <FormulateForm
// v-model="formValues"
// >
// <FormulateInput type="radio" v-model="radioValue" name="foobar" :options="options" />
// </FormulateForm>
// `
// })
// await wrapper.vm.$nextTick()
// await flushPromises()
// expect(wrapper.vm.formValues.foobar).toBe(undefined)
// })
it('does precheck the correct input when radio with options', async () => {
const wrapper = mount({
data () {
return {
radioValue: 'fooey',
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey', gooey: 'Gooey'}
}
},
template: `
<div>
<FormulateInput type="radio" v-model="radioValue" :options="options" />
</div>
`
})
await flushPromises()
const checkboxes = wrapper.findAll('input[type="radio"]:checked')
expect(checkboxes.length).toBe(1)
expect(checkboxes.at(0).element.value).toBe('fooey')
}) })
const fooInputs = wrapper.findAll('input[value^="foo"]')
expect(fooInputs.length).toBe(2)
fooInputs.at(0).setChecked()
await flushPromises()
fooInputs.at(1).setChecked()
await flushPromises()
expect(wrapper.vm.checkboxValues).toEqual(['foo', 'fooey'])
}) })

View File

@ -151,9 +151,9 @@ describe('FormulateInputText', () => {
expect(wrapper.find('input').element.value).toBe('initial val') expect(wrapper.find('input').element.value).toBe('initial val')
}) })
it('uses the v-model value as the initial value when value prop is provided', () => { it('uses the value prop as the initial value when v-model provided', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'text', formulateValue: 'initial val', value: 'initial other val' } }) const wrapper = mount(FormulateInput, { propsData: { type: 'text', formulateValue: 'initial val', value: 'initial other val' } })
expect(wrapper.find('input').element.value).toBe('initial val') expect(wrapper.find('input').element.value).toBe('initial other val')
}) })
it('uses a proxy model internally if it doesnt have a v-model', () => { it('uses a proxy model internally if it doesnt have a v-model', () => {