1
0
mirror of synced 2024-11-25 06:46:02 +03:00

Fixes bug that could cause the wrong repeatable group to be removed

This commit is contained in:
Justin Schroeder 2020-05-18 00:15:45 -04:00
parent d76dc08067
commit bed18767fb
17 changed files with 148 additions and 40 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

4
dist/snow.css vendored
View File

@ -338,7 +338,9 @@
right: .85em; right: .85em;
border-radius: 1.3em; border-radius: 1.3em;
cursor: pointer; 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 { .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: ''; content: '';
position: absolute; position: absolute;

2
dist/snow.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -44,14 +44,12 @@
validation="required|email" validation="required|email"
/> />
</FormulateInput> </FormulateInput>
<FormulateInput
type="submit"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
} }
</script> </script>

View File

@ -24,6 +24,7 @@ import FormulateInputSelect from './inputs/FormulateInputSelect.vue'
import FormulateInputSlider from './inputs/FormulateInputSlider.vue' import FormulateInputSlider from './inputs/FormulateInputSlider.vue'
import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue' import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue'
import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue' import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue'
import FormulateRepeatableRemove from './slots/FormulateRepeatableRemove.vue'
/** /**
* The base formulate library. * The base formulate library.
@ -53,6 +54,7 @@ class Formulate {
FormulateInputSelect, FormulateInputSelect,
FormulateInputSlider, FormulateInputSlider,
FormulateInputTextArea, FormulateInputTextArea,
FormulateRepeatableRemove,
FormulateRepeatableProvider FormulateRepeatableProvider
}, },
slotComponents: { slotComponents: {
@ -60,7 +62,8 @@ class Formulate {
help: 'FormulateHelp', help: 'FormulateHelp',
errors: 'FormulateErrors', errors: 'FormulateErrors',
repeatable: 'FormulateRepeatable', repeatable: 'FormulateRepeatable',
addMore: 'FormulateAddMore' addMore: 'FormulateAddMore',
remove: 'FormulateRepeatableRemove'
}, },
library, library,
rules, rules,

View File

@ -7,8 +7,7 @@
> >
<FormulateRepeatableProvider <FormulateRepeatableProvider
v-for="(item, index) in items" v-for="(item, index) in items"
:ref="`provider-${index}`" :key="item.__id"
:key="index"
:index="index" :index="index"
:set-field-value="(field, value) => setFieldValue(index, field, value)" :set-field-value="(field, value) => setFieldValue(index, field, value)"
:context="context" :context="context"
@ -20,6 +19,7 @@
</template> </template>
<script> <script>
import { setId } from './libs/utils'
export default { export default {
name: 'FormulateGrouping', name: 'FormulateGrouping',
@ -31,7 +31,14 @@ export default {
}, },
provide () { provide () {
return { return {
isSubField: () => true isSubField: () => true,
registerProvider: this.registerProvider,
deregisterProvider: this.deregisterProvider
}
},
data () {
return {
providers: []
} }
}, },
inject: ['formulateRegisterRule', 'formulateRemoveRule'], inject: ['formulateRegisterRule', 'formulateRemoveRule'],
@ -39,14 +46,11 @@ export default {
items () { items () {
if (Array.isArray(this.context.model)) { if (Array.isArray(this.context.model)) {
if (!this.context.repeatable && this.context.model.length === 0) { 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 [{}] return [setId({})]
},
providers () {
return this.items.map((item, i) => Array.isArray(this.$refs[`provider-${i}`]) ? this.$refs[`provider-${i}`][0] : false)
}, },
formShouldShowErrors () { formShouldShowErrors () {
return this.context.formShouldShowErrors return this.context.formShouldShowErrors
@ -72,13 +76,21 @@ export default {
this.formulateRemoveRule('formulateGrouping') this.formulateRemoveRule('formulateGrouping')
}, },
methods: { 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) { setFieldValue (index, field, value) {
const values = Array.isArray(this.context.model) ? this.context.model : [] const values = Array.isArray(this.context.model) ? this.context.model : []
values.splice(index, 1, Object.assign( const previous = this.getAtIndex(index)
{}, const updated = setId(Object.assign({}, previous, { [field]: value }), previous.__id)
typeof this.context.model[index] === 'object' ? this.context.model[index] : {}, values.splice(index, 1, updated)
{ [field]: value }
))
this.context.model = values this.context.model = values
}, },
validateGroup () { validateGroup () {
@ -90,12 +102,20 @@ export default {
}, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors)) }, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors))
}, },
showErrors () { 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) { removeItem (index) {
if (Array.isArray(this.context.model)) { if (Array.isArray(this.context.model)) {
this.context.model.splice(index, 1) 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)
} }
} }
} }

View File

@ -30,6 +30,10 @@ export default {
formulateSetter: (field, value) => this.setFieldValue(field, value) formulateSetter: (field, value) => this.setFieldValue(field, value)
} }
}, },
inject: {
registerProvider: 'registerProvider',
deregisterProvider: 'deregisterProvider'
},
props: { props: {
index: { index: {
type: Number, type: Number,
@ -53,6 +57,12 @@ export default {
computed: { computed: {
...useRegistryComputed() ...useRegistryComputed()
}, },
created () {
this.registerProvider(this)
},
beforeDestroy () {
this.deregisterProvider(this)
},
methods: { methods: {
...useRegistryMethods(['setFieldValue']), ...useRegistryMethods(['setFieldValue']),
removeItem () { removeItem () {

View File

@ -42,6 +42,8 @@
</template> </template>
<script> <script>
import { setId } from '../libs/utils'
export default { export default {
name: 'FormulateInputGroup', name: 'FormulateInputGroup',
props: { props: {
@ -94,10 +96,10 @@ export default {
methods: { methods: {
addItem () { addItem () {
if (Array.isArray(this.context.model)) { if (Array.isArray(this.context.model)) {
this.context.model.push({}) this.context.model.push(setId({}))
return return
} }
this.context.model = this.items.concat([{}]) this.context.model = this.items.concat([setId({})])
}, },
groupItemContext (context, option, groupAttributes) { groupItemContext (context, option, groupAttributes) {
const optionAttributes = {} const optionAttributes = {}

View File

@ -292,7 +292,8 @@ function slotComponents () {
help: this.$formulate.slotComponent(this.type, 'help'), help: this.$formulate.slotComponent(this.type, 'help'),
errors: this.$formulate.slotComponent(this.type, 'errors'), errors: this.$formulate.slotComponent(this.type, 'errors'),
repeatable: this.$formulate.slotComponent(this.type, 'repeatable'), 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')
} }
} }

View File

@ -277,9 +277,10 @@ export function has (ctx, prop) {
} }
/** /**
* Given a registry object, map over it recursively entering groups. * Set a unique Symbol identifier on an object.
* @param {Object} registry key => component * @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') }))
} }

View File

@ -2,13 +2,17 @@
<div <div
class="formulate-input-group-repeatable" class="formulate-input-group-repeatable"
> >
<a <FormulateSlot
v-if="context.repeatable" name="remove"
class="formulate-input-group-repeatable-remove" :context="context"
role="button" :remove-item="removeItem"
@click.prevent="removeItem" >
@keypress.enter="removeItem" <component
:is="context.slotComponents.remove"
:context="context"
:remove-item="removeItem"
/> />
</FormulateSlot>
<slot /> <slot />
</div> </div>
</template> </template>

View 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>

View File

@ -79,6 +79,7 @@ describe('Formulate', () => {
'FormulateInputSelect', 'FormulateInputSelect',
'FormulateInputSlider', 'FormulateInputSlider',
'FormulateInputTextArea', 'FormulateInputTextArea',
'FormulateRepeatableRemove',
'FormulateRepeatableProvider' 'FormulateRepeatableProvider'
] ]
const registry = [] const registry = []

View File

@ -210,6 +210,7 @@ describe('FormulateInputGroup', () => {
}) })
const form = wrapper.findComponent(FormulateForm) const form = wrapper.findComponent(FormulateForm)
await form.vm.formSubmitted() await form.vm.formSubmitted()
await flushPromises()
expect(wrapper.find('[data-classification="text"] .formulate-input-error').exists()).toBe(true); 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) 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 () => { it('does not show an error message on group input when child has an error', async () => {
const wrapper = mount({ const wrapper = mount({
template: ` template: `

View File

@ -422,6 +422,8 @@
border-radius: 1.3em; border-radius: 1.3em;
cursor: pointer; cursor: pointer;
transition: background-color .2s; transition: background-color .2s;
overflow: hidden;
text-indent: -1000px;
&::before, &::before,
&::after { &::after {