From 31007d44183b682131c81174ae4cc3a217579578 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 21 Nov 2023 02:55:29 +0100 Subject: [PATCH] fix(Submit): Make `answers` reactive and fix invalid mutation of computed property Setting `answers` using `.sync` is not reactive as property changes are not triggering the computed / watch effect, so instead use a `onUpdate` method to update the `answers` object in a way that will trigger any watch effect. Also make `getFormValuesFromLocalStorage` a method instead a computed value. Previously that computed value were assigned a new value which is invalid as computed values should be constant (except for computed setter but this was none). And make sure "other" answer is set correctly so it will be saved in LocalStorage Signed-off-by: Ferdinand Thiessen --- src/components/Questions/QuestionMultiple.vue | 89 +++++++++--------- src/views/Submit.vue | 90 +++++++++++++------ 2 files changed, 108 insertions(+), 71 deletions(-) diff --git a/src/components/Questions/QuestionMultiple.vue b/src/components/Questions/QuestionMultiple.vue index 69b64bc67..b6824727d 100644 --- a/src/components/Questions/QuestionMultiple.vue +++ b/src/components/Questions/QuestionMultiple.vue @@ -52,7 +52,7 @@
{{ t('forms', 'Other:') }} - +
@@ -97,9 +98,9 @@
item.startsWith(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX)) - return checkedOtherAnswer[0] !== undefined - }, - }, - - watch: { - inputOtherAnswer() { - if (this.isUnique) { - this.onChange(this.valueOtherAnswer) - return - } - - const values = this.values.filter(item => !item.startsWith(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX)) - if (this.inputOtherAnswer !== '') { - values.push(this.valueOtherAnswer) + /** + * Text of the "other" answer field + */ + otherAnswerText() { + if (this.otherAnswer) { + return this.otherAnswer.slice(QUESTION_EXTRASETTINGS_OTHER_PREFIX.length) } + return '' + }, - this.onChange(values) + /** + * The full "other" answer including prefix, undefined if no "other answer" + */ + otherAnswer() { + return this.values.find((v) => v.startsWith(QUESTION_EXTRASETTINGS_OTHER_PREFIX)) }, }, @@ -228,6 +221,20 @@ export default { this.$emit('update:values', this.isUnique ? [value] : value) }, + /** + * Called when the value of the "other" anwer is changed input + * @param {string} value the new text of the "other" answer + */ + onChangeOther(value) { + // Prefix the value + const prefixedValue = `${QUESTION_EXTRASETTINGS_OTHER_PREFIX}${value}` + // emit the values and add the "other" answer + this.$emit( + 'update:values', + this.isUnique ? [prefixedValue] : [...this.values.filter((v) => !v.startsWith(QUESTION_EXTRASETTINGS_OTHER_PREFIX)), prefixedValue], + ) + }, + /** * Is the provided answer required ? * This is needed for checkboxes as html5 @@ -280,6 +287,7 @@ export default { /** * Update the options + * This will handle updating the form (emitting the changes) and update last changed property * * @param {Array} options options to change */ @@ -304,22 +312,24 @@ export default { /** * Add a new empty answer locally + * @param {InputEvent} event The input event that triggered adding a new entry */ - addNewEntry() { + addNewEntry({ target }) { // Add local entry const options = this.options.slice() options.push({ id: GenRandomId(), questionId: this.id, - text: this.inputValue, + text: target?.value ?? '', local: true, }) - this.inputValue = '' - - // Update question + // Update questions this.updateOptions(options) + // Reset the "new answer" input + this.$refs.pseudoInput.value = '' + this.$nextTick(() => { this.focusIndex(options.length - 1) @@ -412,14 +422,9 @@ export default { * * @param {boolean} allowOtherAnswer show/hide field for other answer */ - onAllowOtherAnswerChange(allowOtherAnswer) { + onAllowOtherAnswerChange(allowOtherAnswer) { return this.onExtraSettingsChange('allowOtherAnswer', allowOtherAnswer) }, - - valueToInputOtherAnswer() { - const otherAnswer = this.values.filter(item => item.startsWith(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX)) - return otherAnswer[0] !== undefined ? otherAnswer[0].substring(this.QUESTION_EXTRASETTINGS_OTHER_PREFIX.length) : '' - }, }, } diff --git a/src/views/Submit.vue b/src/views/Submit.vue index 504b4f033..1e6707e52 100644 --- a/src/views/Submit.vue +++ b/src/views/Submit.vue @@ -95,11 +95,11 @@ :answer-type="answerTypes[question.type]" :index="index + 1" :max-string-lengths="maxStringLengths" + :values="answers[question.id]" v-bind="question" - :values.sync="answers[question.id]" @keydown.enter="onKeydownEnter" @keydown.ctrl.enter="onKeydownCtrlEnter" - @update:values="addFormFieldToLocalStorage(question)" /> + @update:values="(values) => onUpdate(question, values)" /> { // All questions must have a valid title @@ -279,7 +272,7 @@ export default { this.resetData() // Fetch full form on change this.fetchFullForm(this.form.id) - this.initFromLocalHost() + this.initFromLocalStorage() SetWindowTitle(this.formTitle) }, }, @@ -300,15 +293,30 @@ export default { } SetWindowTitle(this.formTitle) if (this.isLoggedIn) { - this.initFromLocalHost() + this.initFromLocalStorage() } }, methods: { - initFromLocalHost() { - if (localStorage.getItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`)) { - for (const key in this.formValuesForLocalStorage) { - const answer = this.formValuesForLocalStorage[key] + /** + * Load saved values for current form from LocalStorage + * @return {Record} + */ + getFormValuesFromLocalStorage() { + const fromLocalStorage = localStorage.getItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`) + if (fromLocalStorage) { + return JSON.parse(fromLocalStorage) + } + return null + }, + + /** + * Initialize answers from saved state in LocalStorage + */ + initFromLocalStorage() { + const savedState = this.getFormValuesFromLocalStorage() + if (savedState) { + for (const [key, answer] of Object.entries(savedState)) { const answers = [] switch (answer?.type) { case 'QuestionMultiple': @@ -324,6 +332,44 @@ export default { } } }, + + /** + * Save updated answers for question to LocalStorage in case of browser crash / closes / etc + * @param {*} question Question to update + */ + addFormFieldToLocalStorage(question) { + if (!this.isLoggedIn) { + return + } + // We make sure the values are updated by the `values.sync` handler + const state = { + ...(this.getFormValuesFromLocalStorage() ?? {}), + [`${question.id}`]: { + value: this.answers[question.id], + type: answerTypes[question.type].component.name, + }, + } + const stringified = JSON.stringify(state) + localStorage.setItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`, stringified) + }, + + deleteFormFieldFromLocalStorage() { + if (!this.isLoggedIn) { + return + } + localStorage.removeItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`) + }, + + /** + * Update answers of a give value + * @param {{id: number}} question The question to answer + * @param {unknown[]} values The new values + */ + onUpdate(question, values) { + this.answers = { ...this.answers, [question.id]: values } + this.addFormFieldToLocalStorage(question) + }, + /** * On Enter, focus next form-element * Last form element is the submit button, the form submits on enter then @@ -346,20 +392,6 @@ export default { // Using button-click event to not bypass validity-checks and use our specified behaviour this.$refs.submitButton.click() }, - addFormFieldToLocalStorage(question) { - if (!this.isLoggedIn) { - return - } - this.formValuesForLocalStorage[`${question.id}`] = { value: this.answers[question.id], type: answerTypes[question.type].component.name } - const parsed = JSON.stringify(this.formValuesForLocalStorage) - localStorage.setItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`, parsed) - }, - deleteFormFieldFromLocalStorage() { - if (!this.isLoggedIn) { - return - } - localStorage.removeItem(`nextcloud_forms_${this.publicView ? this.shareHash : this.hash}`) - }, /* * Methods for catching unwanted unload events