Skip to content

Commit 2a654a0

Browse files
authored
Merge pull request #14 from kevinkosterr/6
Custom select field component
2 parents 9f6b56e + 700490f commit 2a654a0

File tree

5 files changed

+238
-43
lines changed

5 files changed

+238
-43
lines changed

__tests__/components/fields/FieldSelect.spec.js

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,40 @@ describe('Test FieldSelect', () => {
2727

2828
it('Should render correctly', async () => {
2929
const wrapper = mount(FieldSelect, { props })
30-
expect(wrapper.find('select').exists()).toBeTruthy()
31-
expect(wrapper.findAll('option').length).toBe(4)
30+
expect(wrapper.find('.vfg-select').exists()).toBeTruthy()
3231
// First option should be filled with placeholder and value should be empty
33-
expect(wrapper.find('option').element.innerHTML).toContain(props.field.placeholder)
34-
expect(wrapper.find('option').attributes().value).toBe('')
32+
expect(wrapper.find('.vfg-select-label').element.innerHTML).toContain(props.field.placeholder)
3533
})
3634

37-
it('Should render correctly inside form generator', async () => {
38-
config.global.components = { FieldSelect }
39-
40-
const formWrapper = mountFormGenerator(form.schema, form.model)
41-
42-
const selectField = formWrapper.findComponent(FieldSelect)
43-
expect(selectField.exists()).toBeTruthy()
44-
expect(selectField.findAll('option').length).toBe(4)
35+
it('Should open correctly', async () => {
36+
const wrapper = mount(FieldSelect, { props })
37+
const selectEl = wrapper.find('.vfg-select-label')
38+
await selectEl.trigger('click')
39+
expect(wrapper.vm.isOpened).toBeTruthy()
40+
await wrapper.vm.$nextTick()
41+
expect(wrapper.findAll('.vfg-select-option').length).toBe(3)
4542
})
4643

47-
it('Should emit onInput event', async () => {
48-
const wrapper = mount(FieldSelect, { props })
49-
await wrapper.find('select').trigger('change')
50-
expect(wrapper.emitted()).toHaveProperty('onInput')
44+
it('Should render correctly inside form generator', async () => {
45+
config.global.components = { FieldSelect }
46+
const formWrapper = mountFormGenerator(form.schema, props)
47+
const selectComp = formWrapper.findComponent(FieldSelect)
48+
expect(selectComp.exists()).toBeTruthy()
49+
expect(formWrapper.find('.vfg-select').exists()).toBeTruthy()
5150
})
5251

5352
it('Should update model value', async () => {
5453
config.global.components = { FieldSelect }
54+
const formWrapper = mountFormGenerator(form.schema, props)
55+
const selectComp = formWrapper.findComponent(FieldSelect)
56+
expect(selectComp.exists()).toBeTruthy()
5557

56-
const formWrapper = mountFormGenerator(form.schema, form.model)
57-
const selectField = formWrapper.findComponent(FieldSelect)
58-
expect(selectField.exists()).toBeTruthy()
58+
await selectComp.find('.vfg-select-label').trigger('click')
59+
await selectComp.vm.$nextTick()
5960

60-
await selectField.find('select').setValue('test_2')
61-
expect(formWrapper.vm.model.selectModel).toBe('test_2')
62-
await selectField.find('select').setValue('test_3')
63-
expect(formWrapper.vm.model.selectModel).toBe('test_3')
61+
await selectComp.find('.vfg-select-option').trigger('click')
62+
expect(formWrapper.vm.model.selectModel).toBe('test_1')
63+
expect(selectComp.vm.isOpened).toBeFalsy()
6464
})
6565

66-
})
66+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { generateSchemaSingleField, generatePropsSingleField, mountFormGenerator } from '@test/_resources/utils.js'
2+
import { mount, config } from '@vue/test-utils'
3+
import { describe, it, expect } from 'vitest'
4+
5+
import FieldSelect from '@/fields/input/FieldSelectNative.vue'
6+
7+
const form = generateSchemaSingleField(
8+
'testSelect',
9+
'selectModel',
10+
'select',
11+
null,
12+
'What is this?',
13+
'',
14+
{
15+
placeholder: 'Select a test value',
16+
options: [
17+
{ value: 'test_1', name: 'Test 1' },
18+
{ value: 'test_2', name: 'Test 2' },
19+
{ value: 'test_3', name: 'Test 3' }
20+
]
21+
}
22+
)
23+
24+
const props = generatePropsSingleField(form)
25+
26+
describe('Test FieldSelectNative', () => {
27+
28+
it('Should render correctly', async () => {
29+
const wrapper = mount(FieldSelect, { props })
30+
expect(wrapper.find('select').exists()).toBeTruthy()
31+
expect(wrapper.findAll('option').length).toBe(4)
32+
// First option should be filled with placeholder and value should be empty
33+
expect(wrapper.find('option').element.innerHTML).toContain(props.field.placeholder)
34+
expect(wrapper.find('option').attributes().value).toBe('')
35+
})
36+
37+
it('Should render correctly inside form generator', async () => {
38+
config.global.components = { FieldSelect }
39+
40+
const formWrapper = mountFormGenerator(form.schema, form.model)
41+
42+
const selectField = formWrapper.findComponent(FieldSelect)
43+
expect(selectField.exists()).toBeTruthy()
44+
expect(selectField.findAll('option').length).toBe(4)
45+
})
46+
47+
it('Should emit onInput event', async () => {
48+
const wrapper = mount(FieldSelect, { props })
49+
await wrapper.find('select').trigger('change')
50+
expect(wrapper.emitted()).toHaveProperty('onInput')
51+
})
52+
53+
it('Should update model value', async () => {
54+
config.global.components = { FieldSelect }
55+
56+
const formWrapper = mountFormGenerator(form.schema, form.model)
57+
const selectField = formWrapper.findComponent(FieldSelect)
58+
expect(selectField.exists()).toBeTruthy()
59+
60+
await selectField.find('select').setValue('test_2')
61+
expect(formWrapper.vm.model.selectModel).toBe('test_2')
62+
await selectField.find('select').setValue('test_3')
63+
expect(formWrapper.vm.model.selectModel).toBe('test_3')
64+
})
65+
66+
})

src/fields/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import FieldText from '@/fields/input/FieldText.vue'
22
import FieldCheckBox from '@/fields/input/FieldCheckbox.vue'
33
import FieldPassword from '@/fields/input/FieldPassword.vue'
4+
import FieldSelectNative from '@/fields/input/FieldSelectNative.vue'
45
import FieldSelect from '@/fields/input/FieldSelect.vue'
56
import FieldRadio from '@/fields/input/FieldRadio.vue'
67
import FieldColor from '@/fields/input/FieldColor.vue'
@@ -13,7 +14,7 @@ import FieldButton from '@/fields/buttons/FieldButton.vue'
1314

1415

1516
const fieldComponents = [
16-
FieldText, FieldCheckBox, FieldPassword, FieldSelect, FieldRadio, FieldColor, FieldNumber,
17+
FieldText, FieldCheckBox, FieldPassword, FieldSelect, FieldSelectNative, FieldRadio, FieldColor, FieldNumber,
1718
FieldSubmit, FieldReset, FieldButton, FieldSwitch
1819
]
1920

src/fields/input/FieldSelect.vue

Lines changed: 119 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,128 @@
11
<template>
2-
<select
3-
:id="id"
4-
:name="field.name"
5-
:value="currentModelValue"
6-
:required="isRequired"
7-
:disabled="isDisabled"
8-
@change="onFieldValueChanged"
9-
@blur="onBlur"
10-
>
11-
<option disabled value="">
12-
{{ field.placeholder ?? 'Select a ' + field.name }}
13-
</option>
14-
<option v-for="option in field.options" :key="option.value" :value="option.value">
15-
{{ option.name }}
16-
</option>
17-
</select>
2+
<div class="vfg-select">
3+
<span class="vfg-select-label" :class="{'text-muted': !selectedName}" @click.prevent="onClickInput">
4+
<template v-if="selectedName">{{ selectedName }}</template>
5+
<template v-else>{{ field.placeholder || 'Select an option' }}</template>
6+
<span style="float:right;">
7+
<!-- ChevronDown from https://heroicons.com -->
8+
<svg
9+
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
10+
stroke-width="1.5"
11+
stroke="currentColor" style="height: 15px;"
12+
>
13+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
14+
</svg>
15+
</span>
16+
</span>
17+
<div v-if="isOpened" class="vfg-select-list-container">
18+
<div class="vfg-select-list">
19+
<div
20+
v-for="option in field.options"
21+
:key="option.value"
22+
class="vfg-select-option"
23+
:class="{'selected': currentModelValue === option.value}"
24+
@click.prevent="selectOption(option)"
25+
>
26+
{{ option.name }}
27+
</div>
28+
</div>
29+
</div>
30+
</div>
1831
</template>
1932

2033
<script>
2134
import { abstractField } from '@/mixins'
2235
2336
export default {
2437
name: 'FieldSelect',
25-
mixins: [ abstractField ]
38+
mixins: [ abstractField ],
39+
data () {
40+
return {
41+
isOpened: false
42+
}
43+
},
44+
computed: {
45+
selectedName () {
46+
return this.currentModelValue ? this.field.options.find(o => o.value === this.currentModelValue).name : undefined
47+
}
48+
},
49+
methods: {
50+
onClickInput () {
51+
if (this.isOpened) {
52+
this.isOpened = false
53+
return
54+
}
55+
this.isOpened = true
56+
},
57+
selectOption (option) {
58+
this.$emit('onInput', option.value)
59+
this.isOpened = false
60+
}
61+
}
2662
}
27-
</script>
63+
</script>
64+
65+
<style>
66+
.vue-form-generator {
67+
.vfg-select {
68+
display: inline-flex;
69+
cursor: pointer;
70+
position: relative;
71+
user-select: none;
72+
border: 1px solid #cdcdcd;
73+
color: #202020;
74+
border-radius: 5px;
75+
width: 100%;
76+
min-height: 30px;
77+
background: #f4f4f4;
78+
}
79+
80+
.vfg-select-label {
81+
flex: 1 0 70%;
82+
align-content: center;
83+
padding: 0 .3rem;
84+
}
85+
86+
.vfg-select-label.text-muted {
87+
color: #5c5c5c;
88+
}
89+
90+
.vfg-select-list-container {
91+
position: absolute;
92+
padding: .3rem;
93+
top: 32px;
94+
left: 0;
95+
right: 0;
96+
background: #F4F4F4FF;
97+
border-radius: 3px;
98+
max-height: 250px;
99+
overflow-y: auto;
100+
}
101+
102+
.vfg-select-list {
103+
border-radius: inherit;
104+
}
105+
106+
.vfg-select-option {
107+
padding: .3rem .5rem;
108+
margin-bottom: .1rem;
109+
}
110+
111+
.vfg-select-option:first-child, .vfg-select-option:first-child:hover {
112+
border-radius: 3px;
113+
}
114+
115+
.vfg-select-option:last-child, .vfg-select-option:last-child:hover {
116+
border-radius: 3px;
117+
}
118+
119+
.vfg-select-option.selected {
120+
background: #d3d3d3;
121+
}
122+
123+
.vfg-select-option:hover {
124+
background: #d3d3d3;
125+
cursor: pointer;
126+
}
127+
}
128+
</style>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<select
3+
:id="id"
4+
:name="field.name"
5+
:value="currentModelValue"
6+
:required="isRequired"
7+
:disabled="isDisabled"
8+
@change="onFieldValueChanged"
9+
@blur="onBlur"
10+
>
11+
<option disabled value="">
12+
{{ field.placeholder ?? 'Select a ' + field.name }}
13+
</option>
14+
<option v-for="option in field.options" :key="option.value" :value="option.value">
15+
{{ option.name }}
16+
</option>
17+
</select>
18+
</template>
19+
20+
<script>
21+
import { abstractField } from '@/mixins'
22+
23+
export default {
24+
name: 'FieldSelectNative',
25+
mixins: [ abstractField ]
26+
}
27+
</script>

0 commit comments

Comments
 (0)