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;
|
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
2
dist/snow.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 () {
|
||||||
|
@ -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 = {}
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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') }))
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
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',
|
'FormulateInputSelect',
|
||||||
'FormulateInputSlider',
|
'FormulateInputSlider',
|
||||||
'FormulateInputTextArea',
|
'FormulateInputTextArea',
|
||||||
|
'FormulateRepeatableRemove',
|
||||||
'FormulateRepeatableProvider'
|
'FormulateRepeatableProvider'
|
||||||
]
|
]
|
||||||
const registry = []
|
const registry = []
|
||||||
|
@ -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: `
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user