Skip to content

Commit edf2640

Browse files
committed
feat(FieldChecklist): new field
1 parent 1edfc54 commit edf2640

File tree

3 files changed

+193
-2
lines changed

3 files changed

+193
-2
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { mountFormGenerator, generatePropsSingleField, generateSchemaSingleField } from '@test/_resources/utils.js'
2+
import { it, describe, expect, beforeAll } from 'vitest'
3+
import { config, mount } from '@vue/test-utils'
4+
5+
import FieldChecklist from '@/fields/core/FieldChecklist.vue'
6+
7+
const form = generateSchemaSingleField(
8+
'checklistTest',
9+
'checklistModel',
10+
'checklist',
11+
null,
12+
'Test label',
13+
[],
14+
{
15+
options: [
16+
{ name: 'Test 1', value: 'test1' },
17+
{ name: 'Test 2', value: 'test2' },
18+
{ name: 'Test 3', value: 'test3' },
19+
{ name: 'Test 4', value: 'test4' }
20+
]
21+
}
22+
)
23+
24+
const props = generatePropsSingleField(form)
25+
26+
beforeAll(() => {
27+
config.global.components = { FieldChecklist }
28+
})
29+
30+
describe('FieldChecklist', () => {
31+
32+
it('Should render correctly', () => {
33+
const wrapper = mount(FieldChecklist, { props })
34+
const checkBoxes = wrapper.findAll('input[type=checkbox]')
35+
expect(checkBoxes.length).toBe(4)
36+
37+
const labels = wrapper.findAll('label')
38+
expect(labels.length).toBe(4)
39+
40+
// Ensure each label has the checkbox that belongs to them
41+
labels.forEach((label, idx) => {
42+
expect(label.text()).toBe('Test ' + (idx + 1))
43+
const checkbox = label.find('input[type=checkbox]')
44+
expect(checkbox.attributes().value).toBe('test' + (idx + 1))
45+
})
46+
47+
})
48+
49+
it('Should render correctly inside form generator', () => {
50+
const formWrapper = mountFormGenerator(form.schema, form.model)
51+
// The first label should be the label that's specified inside the field's schema.
52+
expect(formWrapper.find('label').text()).toContain('Test label')
53+
54+
const checklistField = formWrapper.findComponent(FieldChecklist)
55+
expect(checklistField.exists()).toBe(true)
56+
57+
const labels = checklistField.findAll('label')
58+
expect(labels.length).toBe(4)
59+
})
60+
61+
it('Should emit onInput event', () => {
62+
const wrapper = mount(FieldChecklist, { props })
63+
wrapper.find('input[type=checkbox]').trigger('change')
64+
expect(wrapper.emitted()).toHaveProperty('onInput')
65+
})
66+
67+
it('Should update model value', async () => {
68+
const formWrapper = mountFormGenerator(form.schema, form.model)
69+
const checklistField = formWrapper.findComponent(FieldChecklist)
70+
expect(checklistField.exists()).toBe(true)
71+
72+
const triggerChange = (value) => formWrapper.find(`input[type=checkbox][value=${value}]`).trigger('change')
73+
74+
triggerChange('test2')
75+
expect(formWrapper.vm.model.checklistModel).toContain('test2')
76+
triggerChange('test3')
77+
expect(formWrapper.vm.model.checklistModel).toContain('test3')
78+
79+
// Value should be removed, since it is already present in the model
80+
triggerChange('test2')
81+
expect(formWrapper.vm.model.checklistModel).not.toContain('test2')
82+
})
83+
84+
it('Should update checked state properly', async () => {
85+
const formWrapper = mountFormGenerator(form.schema, form.model)
86+
const checklistField = formWrapper.findComponent(FieldChecklist)
87+
expect(checklistField.exists()).toBe(true)
88+
89+
const test1Checkbox = () => formWrapper.find('input[type=checkbox][value=test1]')
90+
const test4Checkbox = () => formWrapper.find('input[type=checkbox][value=test4')
91+
92+
// Value shouldn't be checked by default.
93+
expect(test1Checkbox().attributes()).not.toHaveProperty('checked')
94+
test1Checkbox().trigger('change')
95+
await checklistField.vm.$nextTick()
96+
expect(checklistField.find('input[type=checkbox][value=test1]').attributes()).toHaveProperty('checked')
97+
98+
// Value shouldn't be checked by default.
99+
expect(test4Checkbox().attributes()).not.toHaveProperty('checked')
100+
test4Checkbox().trigger('change')
101+
await checklistField.vm.$nextTick()
102+
expect(checklistField.find('input[type=checkbox][value=test4]').attributes()).toHaveProperty('checked')
103+
})
104+
105+
it('Should render values present by default, as checked', async () => {
106+
const model = { checklistModel: [ 'test2', 'test4' ] }
107+
const formWrapper = mountFormGenerator(form.schema, model)
108+
109+
// Values should be set inside the model of the form generator
110+
expect(formWrapper.vm.model.checklistModel).toContain('test2')
111+
expect(formWrapper.vm.model.checklistModel).toContain('test4')
112+
expect(formWrapper.vm.model.checklistModel).toHaveLength(2)
113+
114+
const checklistField = formWrapper.findComponent(FieldChecklist)
115+
expect(checklistField.exists()).toBe(true)
116+
// These values should now be checked, since they are in the model by default
117+
expect(checklistField.find('input[type=checkbox][value=test2').attributes()).toHaveProperty('checked')
118+
expect(checklistField.find('input[type=checkbox][value=test4').attributes()).toHaveProperty('checked')
119+
// These shouldn't be checked, since they are not in the default values
120+
expect(checklistField.find('input[type=checkbox][value=test1').attributes()).not.toHaveProperty('checked')
121+
expect(checklistField.find('input[type=checkbox][value=test3').attributes()).not.toHaveProperty('checked')
122+
})
123+
124+
})

src/fields/core/FieldChecklist.vue

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<template>
2+
<div class="field-checklist">
3+
<div
4+
v-for="checklistItem in field.options"
5+
:key="checklistItem.value"
6+
class="form-checklist"
7+
>
8+
<label class="form-check-label">
9+
<input
10+
type="checkbox"
11+
class="form-check-input"
12+
:value="checklistItem.value"
13+
:checked="currentModelValue.includes(checklistItem.value)"
14+
@change="onFieldValueChanged"
15+
>
16+
{{ checklistItem.name }}
17+
</label>
18+
</div>
19+
</div>
20+
</template>
21+
22+
<script setup>
23+
// This component is heavily inspired by https://github.com/shwld/vfg-field-checkboxlist/
24+
import {
25+
useFieldProps,
26+
useFieldEmits,
27+
useFieldValidate,
28+
useFormModel, useFieldAttributes
29+
} from '@/composables'
30+
import { toRefs } from 'vue'
31+
32+
const emits = defineEmits(useFieldEmits())
33+
const props = defineProps(useFieldProps())
34+
35+
const { field, model } = toRefs(props)
36+
const { hint } = useFieldAttributes(model.value, field.value)
37+
const { currentModelValue } = useFormModel(model.value, field.value)
38+
const { validate, errors } = useFieldValidate(model.value, field.value)
39+
40+
const onFieldValueChanged = ({ target }) => {
41+
errors.value = []
42+
let newValue
43+
const valueAlreadyChecked = currentModelValue.value.includes(target.value)
44+
45+
if (valueAlreadyChecked) {
46+
// If the value was previously already checked, make sure it is removed from the current list of values.
47+
newValue = currentModelValue.value.filter(v => v !== target.value)
48+
} else {
49+
newValue = [ ...currentModelValue.value, target.value ]
50+
}
51+
52+
emits('onInput', newValue)
53+
54+
// Validate the current array of values whenever a value is (un)checked.
55+
validate(newValue).then((validationErrors) => {
56+
emits('validated',
57+
validationErrors.length === 0, // Is the current value for this field valid?
58+
validationErrors, // Actual errors
59+
field.value // Field schema/object
60+
)
61+
})
62+
}
63+
64+
defineExpose({ hint })
65+
</script>

src/fields/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import FieldNumber from '@/fields/core/FieldNumber.vue'
1111
import FieldSwitch from '@/fields/core/FieldSwitch.vue'
1212
import FieldTextarea from '@/fields/core/FieldTextarea.vue'
1313
import FieldMask from '@/fields/core/FieldMask.vue'
14+
import FieldChecklist from '@/fields/core/FieldChecklist.vue'
15+
import FieldCheckbox from '@/fields/core/FieldCheckbox.vue'
1416

1517
import FieldSubmit from '@/fields/core/FieldSubmit.vue'
1618
import FieldReset from '@/fields/core/FieldReset.vue'
@@ -19,8 +21,8 @@ import FieldButton from '@/fields/core/FieldButton.vue'
1921

2022
const fieldComponents = {
2123
FieldColor, FieldText, FieldCheckBox, FieldPassword, FieldSelect, FieldSelectNative, FieldRadio,
22-
FieldNumber, FieldSubmit, FieldReset, FieldButton, FieldSwitch, FieldTextarea, FieldMask
23-
]
24+
FieldNumber, FieldSubmit, FieldReset, FieldButton, FieldSwitch, FieldTextarea, FieldMask, FieldChecklist,
25+
FieldCheckbox
2426
} as const
2527

2628
type FieldComponentNames = keyof typeof fieldComponents

0 commit comments

Comments
 (0)