Fixes issue with grouped options hydrating initial values
This commit is contained in:
parent
1806671194
commit
cf9396e0d6
2
dist/formulate.esm.js
vendored
2
dist/formulate.esm.js
vendored
File diff suppressed because one or more lines are too long
2
dist/formulate.min.js
vendored
2
dist/formulate.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/formulate.umd.js
vendored
2
dist/formulate.umd.js
vendored
File diff suppressed because one or more lines are too long
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -46,11 +46,12 @@ describe('FormulateForm', () => {
|
||||
expect(Object.keys(wrapper.vm.registry)).toEqual(['subinput1', 'subinput2'])
|
||||
})
|
||||
|
||||
it('can set a field’s initial value', () => {
|
||||
it('can set a field’s 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 () {
|
||||
|
@ -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'])
|
||||
})
|
||||
|
@ -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', () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user