1
0
mirror of synced 2025-03-03 03:23:17 +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,
immediate: false
deep: true
}
},
created () {
if (this.hasInitialValue) {
this.internalFormModelProxy = this.initialValues
}
this.applyInitialValues()
},
methods: {
applyInitialValues () {
if (this.hasInitialValue) {
this.internalFormModelProxy = this.initialValues
}
},
setFieldValue (field, value) {
Object.assign(this.internalFormModelProxy, { [field]: value })
this.$emit('input', Object.assign({}, this.internalFormModelProxy))

View File

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

View File

@ -1,7 +1,7 @@
<template>
<div class="formulate-input-group">
<component
:is="optionContext.component"
:is="subComponent"
v-for="optionContext in optionsWithContext"
:key="optionContext.id"
v-model="context.model"
@ -24,16 +24,35 @@ export default {
options () {
return this.context.options || []
},
subComponent () {
// @todo - rough-in for future flexible input-groups
return 'FormulateInput'
},
optionsWithContext () {
const { options, labelPosition, attributes, classification, ...context } = this.context
return this.options.map(option => this.groupItemContext(context, option))
const {
// 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: {
groupItemContext (...args) {
return Object.assign({}, ...args, {
component: 'FormulateInput'
})
groupItemContext (context, option, groupAttributes) {
const optionAttributes = {}
const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes)
return ctx
}
}
}

View File

@ -1,5 +1,5 @@
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
@ -220,7 +220,9 @@ function modelGetter () {
* Set the value from a model.
**/
function modelSetter (value) {
this.internalModelProxy = value
if (!shallowEqualObjects(value, this.internalModelProxy)) {
this.internalModelProxy = value
}
this.$emit('input', value)
if (this.context.name && typeof this.formulateFormSetter === 'function') {
this.formulateFormSetter(this.context.name, value)

View File

@ -46,11 +46,12 @@ describe('FormulateForm', () => {
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, {
propsData: { formulateValue: { testinput: 'has initial value' } },
slots: { default: '<FormulateInput type="text" name="testinput" />' }
})
await flushPromises()
expect(wrapper.find('input').element.value).toBe('has initial value')
})
@ -76,6 +77,31 @@ describe('FormulateForm', () => {
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', () => {
const wrapper = mount({
data () {

View File

@ -8,75 +8,177 @@ import FormulateInputGroup from '@/FormulateInputGroup.vue'
Vue.use(Formulate)
test('type "checkbox" renders a box element', () => {
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' } })
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'} } })
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'} } })
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'} } })
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
test('labelPosition of type "checkbox" defaults to 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'}}})
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'} } })
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'} } })
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
/**
* Test data binding
*/
test('type "checkbox" sets array of values via v-model', 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>
`
describe('FormulateInputBox', () => {
it('renders a box element when type "checkbox" ', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
it('renders a box element when type "radio"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
it('box inputs properly process options object in context library', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
expect(Array.isArray(wrapper.vm.context.options)).toBe(true)
})
it('renders a group when type "checkbox" with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
it('renders a group when type "radio" with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
it('defaults labelPosition to "after" when type "checkbox"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
expect(wrapper.vm.context.labelPosition).toBe('after')
})
it('labelPosition of defaults to before when type "checkbox" with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}})
expect(wrapper.vm.context.labelPosition).toBe('before')
})
it('renders multiple inputs with options when type "radio"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.findAll('input[type="radio"]').length).toBe(2)
})
it('generates ids if not provided when type "radio"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
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"])
})
it('additional context does not bleed through to attributes with type "checkbox" and options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
expect(Object.keys(wrapper.find('input[type="checkbox"]').attributes())).toEqual(["type", "id", "value"])
})
it('allows external attributes to make it down to the inner box elements', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'}, readonly: 'true' } })
expect(Object.keys(wrapper.find('input[type="radio"]').attributes()).includes('readonly')).toBe(true)
})
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')
})
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' } })
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', () => {