1
0
mirror of synced 2024-11-24 22:36: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;
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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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 () {

View File

@ -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 = {}

View File

@ -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')
}
}

View File

@ -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') }))
}

View File

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

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',
'FormulateInputSlider',
'FormulateInputTextArea',
'FormulateRepeatableRemove',
'FormulateRepeatableProvider'
]
const registry = []

View File

@ -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: `

View File

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