Fixes bug that could cause the wrong repeatable group to be removed
This commit is contained in:
parent
d76dc08067
commit
bed18767fb
2
dist/formulate.esm.js
vendored
2
dist/formulate.esm.js
vendored
File diff suppressed because one or more lines are too long
6
dist/formulate.min.js
vendored
6
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
4
dist/snow.css
vendored
4
dist/snow.css
vendored
@ -338,7 +338,9 @@
|
||||
right: .85em;
|
||||
border-radius: 1.3em;
|
||||
cursor: pointer;
|
||||
transition: background-color .2s; }
|
||||
transition: background-color .2s;
|
||||
overflow: hidden;
|
||||
text-indent: -1000px; }
|
||||
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove::before, .formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
2
dist/snow.min.css
vendored
2
dist/snow.min.css
vendored
File diff suppressed because one or more lines are too long
@ -44,14 +44,12 @@
|
||||
validation="required|email"
|
||||
/>
|
||||
</FormulateInput>
|
||||
<FormulateInput
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
@ -24,6 +24,7 @@ import FormulateInputSelect from './inputs/FormulateInputSelect.vue'
|
||||
import FormulateInputSlider from './inputs/FormulateInputSlider.vue'
|
||||
import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue'
|
||||
import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue'
|
||||
import FormulateRepeatableRemove from './slots/FormulateRepeatableRemove.vue'
|
||||
|
||||
/**
|
||||
* The base formulate library.
|
||||
@ -53,6 +54,7 @@ class Formulate {
|
||||
FormulateInputSelect,
|
||||
FormulateInputSlider,
|
||||
FormulateInputTextArea,
|
||||
FormulateRepeatableRemove,
|
||||
FormulateRepeatableProvider
|
||||
},
|
||||
slotComponents: {
|
||||
@ -60,7 +62,8 @@ class Formulate {
|
||||
help: 'FormulateHelp',
|
||||
errors: 'FormulateErrors',
|
||||
repeatable: 'FormulateRepeatable',
|
||||
addMore: 'FormulateAddMore'
|
||||
addMore: 'FormulateAddMore',
|
||||
remove: 'FormulateRepeatableRemove'
|
||||
},
|
||||
library,
|
||||
rules,
|
||||
|
@ -7,8 +7,7 @@
|
||||
>
|
||||
<FormulateRepeatableProvider
|
||||
v-for="(item, index) in items"
|
||||
:ref="`provider-${index}`"
|
||||
:key="index"
|
||||
:key="item.__id"
|
||||
:index="index"
|
||||
:set-field-value="(field, value) => setFieldValue(index, field, value)"
|
||||
:context="context"
|
||||
@ -20,6 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setId } from './libs/utils'
|
||||
|
||||
export default {
|
||||
name: 'FormulateGrouping',
|
||||
@ -31,7 +31,14 @@ export default {
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
isSubField: () => true
|
||||
isSubField: () => true,
|
||||
registerProvider: this.registerProvider,
|
||||
deregisterProvider: this.deregisterProvider
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
providers: []
|
||||
}
|
||||
},
|
||||
inject: ['formulateRegisterRule', 'formulateRemoveRule'],
|
||||
@ -39,14 +46,11 @@ export default {
|
||||
items () {
|
||||
if (Array.isArray(this.context.model)) {
|
||||
if (!this.context.repeatable && this.context.model.length === 0) {
|
||||
return [{}]
|
||||
return [setId({})]
|
||||
}
|
||||
return this.context.model
|
||||
return this.context.model.map(item => setId(item, item.__id))
|
||||
}
|
||||
return [{}]
|
||||
},
|
||||
providers () {
|
||||
return this.items.map((item, i) => Array.isArray(this.$refs[`provider-${i}`]) ? this.$refs[`provider-${i}`][0] : false)
|
||||
return [setId({})]
|
||||
},
|
||||
formShouldShowErrors () {
|
||||
return this.context.formShouldShowErrors
|
||||
@ -72,13 +76,21 @@ export default {
|
||||
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 : []
|
||||
values.splice(index, 1, Object.assign(
|
||||
{},
|
||||
typeof this.context.model[index] === 'object' ? this.context.model[index] : {},
|
||||
{ [field]: value }
|
||||
))
|
||||
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 () {
|
||||
@ -90,12 +102,20 @@ export default {
|
||||
}, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors))
|
||||
},
|
||||
showErrors () {
|
||||
this.providers.map(p => p && typeof p.showErrors === 'function' && p.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,10 @@ export default {
|
||||
formulateSetter: (field, value) => this.setFieldValue(field, value)
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
registerProvider: 'registerProvider',
|
||||
deregisterProvider: 'deregisterProvider'
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
@ -53,6 +57,12 @@ export default {
|
||||
computed: {
|
||||
...useRegistryComputed()
|
||||
},
|
||||
created () {
|
||||
this.registerProvider(this)
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.deregisterProvider(this)
|
||||
},
|
||||
methods: {
|
||||
...useRegistryMethods(['setFieldValue']),
|
||||
removeItem () {
|
||||
|
@ -42,6 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setId } from '../libs/utils'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInputGroup',
|
||||
props: {
|
||||
@ -94,10 +96,10 @@ export default {
|
||||
methods: {
|
||||
addItem () {
|
||||
if (Array.isArray(this.context.model)) {
|
||||
this.context.model.push({})
|
||||
this.context.model.push(setId({}))
|
||||
return
|
||||
}
|
||||
this.context.model = this.items.concat([{}])
|
||||
this.context.model = this.items.concat([setId({})])
|
||||
},
|
||||
groupItemContext (context, option, groupAttributes) {
|
||||
const optionAttributes = {}
|
||||
|
@ -292,7 +292,8 @@ function slotComponents () {
|
||||
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')
|
||||
addMore: this.$formulate.slotComponent(this.type, 'addMore'),
|
||||
remove: this.$formulate.slotComponent(this.type, 'remove')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,9 +277,10 @@ export function has (ctx, prop) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a registry object, map over it recursively entering groups.
|
||||
* @param {Object} registry key => component
|
||||
* Set a unique Symbol identifier on an object.
|
||||
* @param {object} o
|
||||
* @param {Symbol} id
|
||||
*/
|
||||
export function mapRegistry (registry) {
|
||||
//
|
||||
export function setId (o, id) {
|
||||
return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') }))
|
||||
}
|
||||
|
@ -2,13 +2,17 @@
|
||||
<div
|
||||
class="formulate-input-group-repeatable"
|
||||
>
|
||||
<a
|
||||
v-if="context.repeatable"
|
||||
class="formulate-input-group-repeatable-remove"
|
||||
role="button"
|
||||
@click.prevent="removeItem"
|
||||
@keypress.enter="removeItem"
|
||||
<FormulateSlot
|
||||
name="remove"
|
||||
:context="context"
|
||||
:remove-item="removeItem"
|
||||
>
|
||||
<component
|
||||
:is="context.slotComponents.remove"
|
||||
:context="context"
|
||||
:remove-item="removeItem"
|
||||
/>
|
||||
</FormulateSlot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
25
src/slots/FormulateRepeatableRemove.vue
Normal file
25
src/slots/FormulateRepeatableRemove.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<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>
|
@ -79,6 +79,7 @@ describe('Formulate', () => {
|
||||
'FormulateInputSelect',
|
||||
'FormulateInputSlider',
|
||||
'FormulateInputTextArea',
|
||||
'FormulateRepeatableRemove',
|
||||
'FormulateRepeatableProvider'
|
||||
]
|
||||
const registry = []
|
||||
|
@ -210,6 +210,7 @@ describe('FormulateInputGroup', () => {
|
||||
})
|
||||
const form = wrapper.findComponent(FormulateForm)
|
||||
await form.vm.formSubmitted()
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-classification="text"] .formulate-input-error').exists()).toBe(true);
|
||||
})
|
||||
|
||||
@ -348,6 +349,44 @@ describe('FormulateInputGroup', () => {
|
||||
expect(wrapper.findAll('.repeat').length).toBe(1)
|
||||
})
|
||||
|
||||
it('allows a slot override of the remove area', async () => {
|
||||
const wrapper = mount(FormulateInput, {
|
||||
propsData: { type: 'group', repeatable: true, value: [{phone: 'iPhone'}, {phone: 'Android'}]},
|
||||
scopedSlots: {
|
||||
default: '<FormulateInput type="text" name="phone" />',
|
||||
remove: '<button @click="props.removeItem" class="remove-this">Get outta here</button>',
|
||||
}
|
||||
})
|
||||
const repeats = wrapper.findAll('.remove-this')
|
||||
expect(repeats.length).toBe(2)
|
||||
const button = wrapper.find('.remove-this')
|
||||
expect(button.text()).toBe('Get outta here')
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('input').wrappers.map(w => w.element.value)).toEqual(['Android'])
|
||||
})
|
||||
|
||||
it('removes the proper item from the group', async () => {
|
||||
const wrapper = mount(FormulateInput, {
|
||||
propsData: { type: 'group', repeatable: true },
|
||||
slots: {
|
||||
default: '<FormulateInput type="text" name="foo" />'
|
||||
}
|
||||
})
|
||||
wrapper.find('input').setValue('first entry')
|
||||
wrapper.find('.formulate-input-group-add-more button').trigger('click')
|
||||
wrapper.find('.formulate-input-group-add-more button').trigger('click')
|
||||
await flushPromises();
|
||||
wrapper.findAll('input').at(1).setValue('second entry')
|
||||
wrapper.findAll('input').at(2).setValue('third entry')
|
||||
// First verify all the proper entries are where we expect
|
||||
expect(wrapper.findAll('input').wrappers.map(input => input.element.value)).toEqual(['first entry', 'second entry', 'third entry'])
|
||||
// Now remove the middle one
|
||||
wrapper.findAll('.formulate-input-group-repeatable-remove').at(1).trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('input').wrappers.map(input => input.element.value)).toEqual(['first entry', 'third entry'])
|
||||
})
|
||||
|
||||
it('does not show an error message on group input when child has an error', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
|
@ -422,6 +422,8 @@
|
||||
border-radius: 1.3em;
|
||||
cursor: pointer;
|
||||
transition: background-color .2s;
|
||||
overflow: hidden;
|
||||
text-indent: -1000px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
|
Loading…
Reference in New Issue
Block a user