Skip to content

Commit 0f125fa

Browse files
committed
enh: improve error messages for required questions.
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
1 parent a138698 commit 0f125fa

9 files changed

Lines changed: 121 additions & 51 deletions

src/components/Questions/QuestionColor.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
v-bind="questionProps"
99
:titlePlaceholder="answerType.titlePlaceholder"
1010
:warningInvalid="answerType.warningInvalid"
11+
:errorMessage="errorMessage"
1112
v-on="commonListeners">
1213
<div
1314
class="question__content"
@@ -17,6 +18,7 @@
1718
<NcColorPicker
1819
:modelValue="pickedColor"
1920
advancedFields
21+
:aria-required="isRequired"
2022
@update:modelValue="onUpdatePickedColor">
2123
<NcButton :disabled="!readOnly">
2224
{{ colorPickerPlaceholder }}
@@ -87,6 +89,16 @@ export default {
8789
},
8890
8991
methods: {
92+
async validate() {
93+
if (this.isRequired && this.pickedColor === '') {
94+
this.errorMessage = t('forms', 'You must answer this question')
95+
return false
96+
}
97+
98+
this.errorMessage = null
99+
return true
100+
},
101+
90102
onUpdatePickedColor(color) {
91103
this.$emit('update:values', [color])
92104
},

src/components/Questions/QuestionDate.vue

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
v-bind="questionProps"
99
:titlePlaceholder="answerType.titlePlaceholder"
1010
:warningInvalid="answerType.warningInvalid"
11+
:errorMessage="errorMessage"
1112
v-on="commonListeners">
1213
<template v-if="answerType.pickerType === 'date'" #actions>
1314
<NcActionCheckbox
@@ -95,7 +96,8 @@
9596
:type="dateTimePickerType"
9697
:disabledDate="disabledDates"
9798
:disabledTime="disabledTimes"
98-
rangeSeparator=" - "
99+
:aria-required="isRequired"
100+
clearable
99101
@update:modelValue="onValueChange" />
100102
</div>
101103
<template #insert>
@@ -146,25 +148,31 @@ export default {
146148
},
147149
148150
computed: {
151+
isRangeQuestion() {
152+
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
153+
? true
154+
: false
155+
},
156+
149157
datetimePickerPlaceholder() {
150158
if (this.readOnly) {
151-
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
159+
return this.isRangeQuestion
152160
? this.answerType.submitPlaceholderRange
153161
: this.answerType.submitPlaceholder
154162
}
155-
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
163+
return this.isRangeQuestion
156164
? this.answerType.createPlaceholderRange
157165
: this.answerType.createPlaceholder
158166
},
159167
160168
dateTimePickerType() {
161-
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
169+
return this.isRangeQuestion
162170
? this.answerType.pickerType + '-range'
163171
: this.answerType.pickerType
164172
},
165173
166174
time() {
167-
if (this.extraSettings?.dateRange || this.extraSettings?.timeRange) {
175+
if (this.isRangeQuestion) {
168176
return this.values?.[0]
169177
? [this.parse(this.values[0]), this.parse(this.values[1])]
170178
: null
@@ -224,14 +232,27 @@ export default {
224232
},
225233
226234
methods: {
235+
async validate() {
236+
if (this.isRequired && this.time === null) {
237+
this.errorMessage = t('forms', 'You must answer this question')
238+
return false
239+
}
240+
241+
this.errorMessage = null
242+
return true
243+
},
244+
227245
/**
228246
* DateTimepicker show text in picker
229247
* Format depends on component-type date/datetime
230248
*
231-
* @param {Date} date the selected datepicker Date
249+
* @param {Date|Date[]} date the selected datepicker Date
232250
* @return {string}
233251
*/
234252
stringify(date) {
253+
if (this.isRangeQuestion && Array.isArray(date)) {
254+
return `${moment(date[0]).format(this.answerType.momentFormat)} - ${moment(date[1]).format(this.answerType.momentFormat)}`
255+
}
235256
return moment(date).format(this.answerType.momentFormat)
236257
},
237258
@@ -335,10 +356,15 @@ export default {
335356
/**
336357
* Store Value
337358
*
338-
* @param {Date|Array<Date>} date The date or date range to store
359+
* @param {Date|Array<Date>|null} date The date or date range to store
339360
*/
340361
onValueChange(date) {
341-
if (this.extraSettings?.dateRange || this.extraSettings?.timeRange) {
362+
if (!date) {
363+
this.$emit('update:values', [])
364+
return
365+
}
366+
367+
if (this.isRangeQuestion) {
342368
this.$emit('update:values', [
343369
moment(date[0]).format(this.answerType.storageFormat),
344370
moment(date[1]).format(this.answerType.storageFormat),

src/components/Questions/QuestionDropdown.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
:warningInvalid="answerType.warningInvalid"
1111
:contentValid="contentValid"
1212
:shiftDragHandle="shiftDragHandle"
13+
:errorMessage="errorMessage"
1314
v-on="commonListeners">
1415
<template #actions>
1516
<NcActionCheckbox
@@ -40,6 +41,7 @@
4041
:searchable="false"
4142
label="text"
4243
:aria-label-combobox="selectOptionPlaceholder"
44+
@invalid.prevent="validate"
4345
@update:modelValue="onInput" />
4446
</div>
4547
<template v-else>
@@ -184,6 +186,16 @@ export default {
184186
},
185187
186188
methods: {
189+
async validate() {
190+
if (this.isRequired && this.areNoneChecked) {
191+
this.errorMessage = t('forms', 'You must answer this question')
192+
return false
193+
}
194+
195+
this.errorMessage = null
196+
return true
197+
},
198+
187199
onDragStart() {
188200
this.isDragging = true
189201
},

src/components/Questions/QuestionFile.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,12 @@
118118
ref="fileInput"
119119
class="hidden-visually"
120120
type="file"
121+
:required="isRequired && values.length === 0"
121122
:disabled="!readOnly"
122123
:multiple="maxAllowedFilesCount > 1"
123124
:name="name || undefined"
124125
:accept="accept.length ? accept.join(',') : null"
126+
@invalid.prevent="validate"
125127
@input="onFileInput" />
126128
</label>
127129
<NcButton
@@ -422,6 +424,12 @@ export default {
422424
)
423425
return false
424426
}
427+
428+
if (this.isRequired && this.values.length === 0) {
429+
this.errorMessage = t('forms', 'You must answer this question')
430+
return false
431+
}
432+
425433
this.errorMessage = null
426434
return true
427435
},

src/components/Questions/QuestionGrid.vue

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ export default {
264264
265265
methods: {
266266
async validate() {
267+
if (this.isRequired && this.areNoneChecked) {
268+
this.errorMessage = t('forms', 'You must answer this question')
269+
return false
270+
}
271+
267272
if (!this.isUnique) {
268273
// Validate limits
269274
const max = this.extraSettings.optionsLimitMax ?? 0
@@ -315,30 +320,6 @@ export default {
315320
316321
this.$emit('update:values', values)
317322
},
318-
319-
/**
320-
* Is the provided answer required ?
321-
* This is needed for checkboxes as html5
322-
* doesn't allow to require at least ONE checked.
323-
* So we require the one that are checked or all
324-
* if none are checked yet.
325-
*
326-
* @return {boolean}
327-
*/
328-
checkRequired() {
329-
// false, if question not required
330-
if (!this.isRequired) {
331-
return false
332-
}
333-
334-
// true for Radiobuttons
335-
if (this.isUnique) {
336-
return true
337-
}
338-
339-
// For checkboxes, only required if no other is checked
340-
return this.areNoneChecked
341-
},
342323
},
343324
}
344325
</script>

src/components/Questions/QuestionLinearScale.vue

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
v-bind="questionProps"
99
:titlePlaceholder="answerType.titlePlaceholder"
1010
:warningInvalid="answerType.warningInvalid"
11+
:errorMessage="errorMessage"
1112
v-on="commonListeners">
1213
<template #actions>
1314
<NcActionInput
@@ -85,7 +86,8 @@
8586
:value="option.toString()"
8687
:name="`${id}-answer`"
8788
type="radio"
88-
:required="checkRequired(option)"
89+
:required="isRequired"
90+
@invalid.prevent="validate"
8991
@update:modelValue="onChange"
9092
@keydown.enter.exact.prevent="onKeydownEnter" />
9193
</div>
@@ -203,6 +205,16 @@ export default {
203205
},
204206
205207
methods: {
208+
async validate() {
209+
if (this.isRequired && this.values.length === 0) {
210+
this.errorMessage = t('forms', 'You must answer this question')
211+
return false
212+
}
213+
214+
this.errorMessage = null
215+
return true
216+
},
217+
206218
onChange(option) {
207219
this.$emit('update:values', [option])
208220
},
@@ -231,24 +243,6 @@ export default {
231243
})
232244
},
233245
234-
/**
235-
* Is the provided answer required ?
236-
* This is needed for checkboxes as html5
237-
* doesn't allow to require at least ONE checked.
238-
* So we require the one that are checked or all
239-
* if none are checked yet.
240-
*
241-
* @return {boolean}
242-
*/
243-
checkRequired() {
244-
// false, if question not required
245-
if (!this.isRequired) {
246-
return false
247-
}
248-
249-
return true
250-
},
251-
252246
/**
253247
* Resizes the given label to fit within the specified constraints.
254248
*

src/components/Questions/QuestionLong.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
v-bind="questionProps"
99
:titlePlaceholder="answerType.titlePlaceholder"
1010
:warningInvalid="answerType.warningInvalid"
11+
:errorMessage="errorMessage"
1112
v-on="commonListeners">
1213
<div class="question__content">
1314
<textarea
@@ -23,6 +24,7 @@
2324
:maxlength="maxStringLengths.answerText"
2425
minlength="1"
2526
:name="name || undefined"
27+
@invalid.prevent="validate"
2628
@input="onInput"
2729
@keypress="autoSizeText"
2830
@keydown.ctrl.enter="onKeydownCtrlEnter" />
@@ -75,6 +77,19 @@ export default {
7577
},
7678
7779
methods: {
80+
async validate() {
81+
if (
82+
this.isRequired
83+
&& (this.values.length === 0 || this.values[0] === '')
84+
) {
85+
this.errorMessage = t('forms', 'You must answer this question')
86+
return false
87+
}
88+
89+
this.errorMessage = null
90+
return true
91+
},
92+
7893
onInput() {
7994
const textarea = this.$refs.textarea
8095
this.$emit('update:values', [textarea.value])

src/components/Questions/QuestionMultiple.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
:name="`${id}-answer`"
8686
:type="isUnique ? 'radio' : 'checkbox'"
8787
:required="checkRequired(answer.id)"
88+
@invalid.prevent="validate"
8889
@update:modelValue="onChange"
8990
@keydown.enter.exact.prevent="onKeydownEnter">
9091
{{ answer.text }}
@@ -99,6 +100,7 @@
99100
:type="isUnique ? 'radio' : 'checkbox'"
100101
:required="checkRequired('other-answer')"
101102
class="question__label"
103+
@invalid.prevent="validate"
102104
@update:modelValue="onChangeOther"
103105
@keydown.enter.exact.prevent="onKeydownEnter">
104106
{{ t('forms', 'Other:') }}
@@ -361,6 +363,11 @@ export default {
361363
362364
methods: {
363365
async validate() {
366+
if (this.isRequired && this.areNoneChecked) {
367+
this.errorMessage = t('forms', 'You must answer this question')
368+
return false
369+
}
370+
364371
if (!this.isUnique) {
365372
// Validate limits
366373
const max = this.extraSettings.optionsLimitMax ?? 0

src/components/Questions/QuestionShort.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
v-bind="questionProps"
99
:titlePlaceholder="answerType.titlePlaceholder"
1010
:warningInvalid="answerType.warningInvalid"
11+
:errorMessage="errorMessage"
1112
v-on="commonListeners">
1213
<div class="question__content">
1314
<input
@@ -25,6 +26,7 @@
2526
minlength="1"
2627
:type="validationObject.inputType"
2728
:step="validationObject.inputType === 'number' ? 'any' : undefined"
29+
@invalid.prevent="validate"
2830
@input="onInput"
2931
@keydown.enter.exact.prevent="onKeydownEnter" />
3032
<NcActions
@@ -156,6 +158,19 @@ export default {
156158
},
157159
158160
methods: {
161+
async validate() {
162+
if (
163+
this.isRequired
164+
&& (this.values.length === 0 || this.values[0] === '')
165+
) {
166+
this.errorMessage = t('forms', 'You must answer this question')
167+
return false
168+
}
169+
170+
this.errorMessage = null
171+
return true
172+
},
173+
159174
onInput() {
160175
/** @type {HTMLObjectElement} */
161176
const input = this.$refs.input

0 commit comments

Comments
 (0)