diff --git a/src/question.ts b/src/question.ts index 7726cda74b..d0da4bf115 100644 --- a/src/question.ts +++ b/src/question.ts @@ -78,6 +78,7 @@ export class Question extends SurveyElement private locProcessedTitle: LocalizableString; protected isReadyValue: boolean = true; private commentElements: Array; + private dependedQuestions: Array = []; /** * An event that is raised when the question's ready state has changed (expressions are evaluated, choices are loaded from a web resource specified by the `choicesByUrl` property, etc.). @@ -268,6 +269,24 @@ export class Question extends SurveyElement this.removeSelfFromList(this.parent.elements); } } + protected addDependedQuestion(question: Question): void { + if (!question || this.dependedQuestions.indexOf(question) > -1) return; + this.dependedQuestions.push(question); + } + protected removeDependedQuestion(question: Question): void { + if (!question) return; + var index = this.dependedQuestions.indexOf(question); + if (index > -1) { + this.dependedQuestions.splice(index, 1); + } + } + protected updateDependedQuestions(): void { + for (var i = 0; i < this.dependedQuestions.length; i++) { + this.dependedQuestions[i].updateDependedQuestion(); + } + } + protected updateDependedQuestion(): void {} + protected resetDependedQuestion(): void {} public get isFlowLayout(): boolean { return this.getLayoutType() === "flow"; } @@ -1264,6 +1283,7 @@ export class Question extends SurveyElement this.setPropertyValue("comment", val); this.fireCallback(this.commentChangedCallback); } + public get isValueArray(): boolean { return false; } /** * Gets or sets the question value. * @see SurveyModel.setValue @@ -2235,8 +2255,11 @@ export class Question extends SurveyElement this.renderAs = this.getDesktopRenderAs(); } } - public dispose() { + public dispose(): void { super.dispose(); + for (var i = 0; i < this.dependedQuestions.length; i++) { + this.dependedQuestions[i].resetDependedQuestion(); + } this.destroyResizeObserver(); } } diff --git a/src/question_baseselect.ts b/src/question_baseselect.ts index f8ac6902a9..2aa24fe56b 100644 --- a/src/question_baseselect.ts +++ b/src/question_baseselect.ts @@ -29,7 +29,6 @@ export class QuestionSelectBase extends Question { private cachedValueForUrlRequests: any; private isChoicesLoaded: boolean; private enableOnLoadingChoices: boolean; - private dependedQuestions: Array = []; private noneItemValue: ItemValue = new ItemValue(settings.noneItemValue); private newItemValue: ItemValue; private canShowOptionItemCallback: (item: ItemValue) => boolean; @@ -57,7 +56,7 @@ export class QuestionSelectBase extends Question { } }); this.registerPropertyChangedHandlers( - ["choicesFromQuestion", "choicesFromQuestionMode", "showNoneItem"], + ["choicesFromQuestion", "choicesFromQuestionMode", "choicesFromValueName", "choicesFromTextName", "showNoneItem"], () => { this.onVisibleChoicesChanged(); } @@ -93,12 +92,15 @@ export class QuestionSelectBase extends Question { public getType(): string { return "selectbase"; } - public dispose() { + public dispose(): void { super.dispose(); - for (var i = 0; i < this.dependedQuestions.length; i++) { - this.dependedQuestions[i].choicesFromQuestion = ""; + const q = this.getQuestionWithChoices(); + if(!!q) { + q.removeDependedQuestion(this); } - this.removeFromDependedQuestion(this.getQuestionWithChoices()); + } + protected resetDependedQuestion(): void { + this.choicesFromQuestion = ""; } public get otherId(): string { return this.id + "_other"; @@ -116,24 +118,28 @@ export class QuestionSelectBase extends Question { return res; } public get isUsingCarryForward(): boolean { - return this.getPropertyValue("isUsingCarrayForward", false); + return !!this.carryForwardQuestionType; + } + public get carryForwardQuestionType(): string { + return this.getPropertyValue("carryForwardQuestionType"); } - private setIsUsingCarrayForward(val: boolean): void { - this.setPropertyValue("isUsingCarrayForward", val); + private setCarryForwardQuestionType(selBaseQuestion: boolean, arrayQuestion: boolean): void { + const mode = selBaseQuestion ? "select" : (arrayQuestion ? "array" : undefined); + this.setPropertyValue("carryForwardQuestionType", mode); } - public supportGoNextPageError() { + public supportGoNextPageError(): boolean { return !this.isOtherSelected || !!this.otherValue; } isLayoutTypeSupported(layoutType: string): boolean { return true; } - public localeChanged() { + public localeChanged(): void { super.localeChanged(); if (this.choicesOrder !== "none") { this.updateVisibleChoices(); } } - public locStrsChanged() { + public locStrsChanged(): void { super.locStrsChanged(); if (!!this.choicesFromUrl) { ItemValue.locStrsChanged(this.choicesFromUrl); @@ -689,23 +695,12 @@ export class QuestionSelectBase extends Question { var question = this.getQuestionWithChoices(); this.isLockVisibleChoices = !!question && question.name === val; if (!!question && question.name !== val) { - question.removeFromDependedQuestion(this); + question.removeDependedQuestion(this); } this.setPropertyValue("choicesFromQuestion", val); this.isLockVisibleChoices = false; } private isLockVisibleChoices: boolean; - private addIntoDependedQuestion(question: QuestionSelectBase) { - if (!question || question.dependedQuestions.indexOf(this) > -1) return; - question.dependedQuestions.push(this); - } - private removeFromDependedQuestion(question: QuestionSelectBase) { - if (!question) return; - var index = question.dependedQuestions.indexOf(this); - if (index > -1) { - question.dependedQuestions.splice(index, 1); - } - } /** * Specifies which choice items to inherit from another question. Applies only when the `choicesFromQuestion` property is specified. * @@ -725,6 +720,18 @@ export class QuestionSelectBase extends Question { public set choicesFromQuestionMode(val: string) { this.setPropertyValue("choicesFromQuestionMode", val); } + public get choicesFromValueName(): string { + return this.getPropertyValue("choicesFromValueName"); + } + public set choicesFromValueName(val: string) { + this.setPropertyValue("choicesFromValueName", val); + } + public get choicesFromTextName(): string { + return this.getPropertyValue("choicesFromTextName"); + } + public set choicesFromTextName(val: string) { + this.setPropertyValue("choicesFromTextName", val); + } /** * Specifies whether to hide the question if no choice items are visible. * @@ -1012,29 +1019,66 @@ export class QuestionSelectBase extends Question { : this.activeChoices; } protected get activeChoices(): Array { - const question = this.getQuestionWithChoices(); - this.setIsUsingCarrayForward(!!question); - if (this.isUsingCarryForward) { - this.addIntoDependedQuestion(question); - return this.getChoicesFromQuestion(question); + const question = this.findCarryForwardQuestion(); + const selBaseQuestion = this.getQuestionWithChoicesCore(question); + const arrayQuestion = !selBaseQuestion ? this.getQuestionWithArrayValue(question) : null; + this.setCarryForwardQuestionType(!!selBaseQuestion, !!arrayQuestion); + if (this.carryForwardQuestionType === "select") { + selBaseQuestion.addDependedQuestion(this); + return this.getChoicesFromSelectQuestion(selBaseQuestion); + } + if (this.carryForwardQuestionType === "array") { + (arrayQuestion).addDependedQuestion(this); + return this.getChoicesFromArrayQuestion(arrayQuestion); } return this.choicesFromUrl ? this.choicesFromUrl : this.getChoices(); } private getQuestionWithChoices(): QuestionSelectBase { + return this.getQuestionWithChoicesCore(this.findCarryForwardQuestion()); + } + private findCarryForwardQuestion(): Question { if (!this.choicesFromQuestion || !this.data) return null; - var res: any = this.data.findQuestionByName(this.choicesFromQuestion); - return !!res && !!res.visibleChoices && Array.isArray(res.dependedQuestions) && res !== this ? res : null; + return this.data.findQuestionByName(this.choicesFromQuestion); + } + private getQuestionWithChoicesCore(question: Question): QuestionSelectBase { + if(!!question && !!question.visibleChoices && (Serializer.isDescendantOf(question.getType(), "selectbase")) && question !== this) + return question; + return null; + } + private getQuestionWithArrayValue(question: Question): Question { + return !!question && question.isValueArray ? question : null; + } + private getChoicesFromArrayQuestion(question: Question): Array { + if (this.isDesignMode) return []; + const val = question.value; + if(!Array.isArray(val)) return []; + const res: Array = []; + for(var i = 0; i < val.length; i ++) { + const obj = val[i]; + if(!Helpers.isValueObject(obj)) continue; + const key = this.getValueKeyName(obj); + if(!!key && !this.isValueEmpty(obj[key])) { + const text = !!this.choicesFromTextName ? obj[this.choicesFromTextName] : undefined; + res.push(this.createItemValue(obj[key], text)); + } + } + return res; + } + private getValueKeyName(obj: any): string { + if(this.choicesFromValueName) return this.choicesFromValueName; + const keys = Object.keys(obj); + return keys.length > 0 ? keys[0] : undefined; } - private getChoicesFromQuestion(question: QuestionSelectBase): Array { + private getChoicesFromSelectQuestion(question: QuestionSelectBase): Array { if (this.isDesignMode) return []; - var res: Array = []; + const res: Array = []; var isSelected = this.choicesFromQuestionMode == "selected" ? true : this.choicesFromQuestionMode == "unselected" ? false : undefined; - var choices = question.visibleChoices; + const choices = question.visibleChoices; for (var i = 0; i < choices.length; i++) { if (this.isBuiltInChoice(choices[i], question)) continue; if (isSelected === undefined) { @@ -1339,13 +1383,13 @@ export class QuestionSelectBase extends Question { if (this.isLoadingFromJson || this.isUpdatingChoicesDependedQuestions || !this.allowNotifyValueChanged || this.choicesByUrl.isRunning) return; this.isUpdatingChoicesDependedQuestions = true; - for (var i = 0; i < this.dependedQuestions.length; i++) { - const q = this.dependedQuestions[i]; - q.onVisibleChoicesChanged(); - q.clearIncorrectValues(); - } + this.updateDependedQuestions(); this.isUpdatingChoicesDependedQuestions = false; } + protected updateDependedQuestion(): void { + this.onVisibleChoicesChanged(); + this.clearIncorrectValues(); + } onSurveyValueChanged(newValue: any) { super.onSurveyValueChanged(newValue); this.updateChoicesDependedQuestions(); @@ -1738,7 +1782,7 @@ Serializer.addClass( "selectbase", [ { name: "showCommentArea:switch", layout: "row", visible: true, category: "general" }, - "choicesFromQuestion:question_selectbase", + "choicesFromQuestion:question_carryforward", { name: "choices:itemvalue[]", uniqueProperty: "value", baseValue: function () { @@ -1755,7 +1799,21 @@ Serializer.addClass( choices: ["all", "selected", "unselected"], dependsOn: "choicesFromQuestion", visibleIf: (obj: any) => { - return !!obj.choicesFromQuestion; + return obj.carryForwardQuestionType === "select"; + }, + }, + { + name: "choicesFromValueName", + dependsOn: "choicesFromQuestion", + visibleIf: (obj: any) => { + return obj.carryForwardQuestionType === "array"; + }, + }, + { + name: "choicesFromTextName", + dependsOn: "choicesFromQuestion", + visibleIf: (obj: any) => { + return obj.carryForwardQuestionType === "array"; }, }, { diff --git a/src/question_checkbox.ts b/src/question_checkbox.ts index 279bdc32b1..3c5b5c6253 100644 --- a/src/question_checkbox.ts +++ b/src/question_checkbox.ts @@ -169,6 +169,7 @@ export class QuestionCheckboxModel extends QuestionCheckboxBase { if (!val) return val; return !this.valuePropertyName ? val : val[this.valuePropertyName]; } + public get isValueArray(): boolean { return true; } /** * Specifies the maximum number of selected choices. * diff --git a/src/question_matrixdynamic.ts b/src/question_matrixdynamic.ts index 6571b96f73..4b539937d9 100644 --- a/src/question_matrixdynamic.ts +++ b/src/question_matrixdynamic.ts @@ -128,6 +128,7 @@ export class QuestionMatrixDynamicModel extends QuestionMatrixDropdownModelBase public set confirmDelete(val: boolean) { this.setPropertyValue("confirmDelete", val); } + public get isValueArray(): boolean { return true; } /** * Specifies a key column. Set this property to a column name, and the question will display `keyDuplicationError` if a user tries to enter a duplicate value in this column. * @see keyDuplicationError diff --git a/src/question_paneldynamic.ts b/src/question_paneldynamic.ts index e1cc8f5670..cc19a6bd04 100644 --- a/src/question_paneldynamic.ts +++ b/src/question_paneldynamic.ts @@ -1137,6 +1137,7 @@ export class QuestionPanelDynamicModel extends Question } this.value = newValue; } + public get isValueArray(): boolean { return true; } public isEmpty(): boolean { var val = this.value; if (!val || !Array.isArray(val)) return true; diff --git a/tests/question_baseselecttests.ts b/tests/question_baseselecttests.ts index a316e7e16b..32053b4861 100644 --- a/tests/question_baseselecttests.ts +++ b/tests/question_baseselecttests.ts @@ -10,6 +10,7 @@ import { defaultV2Css } from "../src/defaultCss/defaultV2Css"; import { IAction } from "../src/actions/action"; import { surveyLocalization } from "../src/surveyStrings"; import { Base } from "../src/base"; +import { QuestionMatrixDynamicModel } from "../src/question_matrixdynamic"; export default QUnit.module("baseselect"); @@ -1065,3 +1066,110 @@ QUnit.test("displayValue & otherItem", function (assert) { q2.comment = "Some comments"; assert.equal(q2.displayValue, "Some comments, 1", "#4"); }); +QUnit.test("Use carryForward with matrix dynamic", function (assert) { + const survey = new SurveyModel({ elements: [ + { type: "matrixdynamic", name: "q1", columns: [{ name: "col1", cellType: "text" }] }, + { type: "checkbox", name: "q2", choicesFromQuestion: "q1" } + ] }); + const q1 = survey.getQuestionByName("q1"); + const q2 = survey.getQuestionByName("q2"); + assert.equal(q2.choicesFromQuestion, "q1", "choicesFromQuestion is set"); + assert.equal(q2.isUsingCarryForward, true, "Carryforward flag is set"); + assert.equal(q2.visibleChoices.length, 0, "There is no choices"); + assert.equal(q2.visibleChoices.length, 0, "There is no choices, row is empty"); + q1.visibleRows[0].cells[0].value = "A"; + assert.deepEqual(survey.data, { q1: [{ col1: "A" }, {}] }, "survey.data is correct"); + assert.equal(q2.visibleChoices.length, 1, "There is one choice"); + assert.equal(q2.visibleChoices[0].value, "A", "the first value is correct"); + q1.visibleRows[1].cells[0].value = "B"; + assert.equal(q2.visibleChoices.length, 2, "There are two choice"); + assert.equal(q2.visibleChoices[1].value, "B", "the second value is correct"); + q1.addRow(); + assert.equal(q2.visibleChoices.length, 2, "There are two choice, new row is empty"); + q1.visibleRows[2].cells[0].value = "C"; + assert.deepEqual(survey.data, { q1: [{ col1: "A" }, { col1: "B" }, { col1: "C" }] }, "survey.data is correct, #2"); + assert.equal(q2.visibleChoices.length, 3, "There are three choice"); + assert.equal(q2.visibleChoices[2].value, "C", "the third value is correct"); +}); + +QUnit.test("Use carryForward with matrix dynamic + choicesFromValueName", function (assert) { + const survey = new SurveyModel({ elements: [ + { type: "matrixdynamic", name: "q1", columns: [{ name: "col1", cellType: "text" }, { name: "col2", cellType: "text" }] }, + { type: "checkbox", name: "q2", choicesFromQuestion: "q1", choicesFromValueName: "col2" } + ] }); + const q1 = survey.getQuestionByName("q1"); + const q2 = survey.getQuestionByName("q2"); + assert.equal(q2.choicesFromQuestion, "q1", "choicesFromQuestion is set"); + assert.equal(q2.choicesFromValueName, "col2", "choicesFromValueName is set"); + assert.equal(q2.isUsingCarryForward, true, "Carryforward flag is set"); + assert.equal(q2.visibleChoices.length, 0, "There is no choices"); + assert.equal(q2.visibleChoices.length, 0, "There is no choices, row is empty"); + q1.visibleRows[0].cells[0].value = "A"; + assert.equal(q2.visibleChoices.length, 0, "There is no choices, col2 is empty"); + q1.visibleRows[0].cells[1].value = "AA"; + assert.equal(q2.visibleChoices.length, 1, "There is one choice"); + assert.equal(q2.visibleChoices[0].value, "AA", "the first value is correct"); + q1.visibleRows[1].cells[0].value = "B"; + q1.visibleRows[1].cells[1].value = "BB"; + assert.equal(q2.visibleChoices.length, 2, "There are two choice"); + assert.equal(q2.visibleChoices[1].value, "BB", "the second value is correct"); + q1.addRow(); + assert.equal(q2.visibleChoices.length, 2, "There are two choice, new row is empty"); + q1.visibleRows[2].cells[0].value = "C"; + assert.equal(q2.visibleChoices.length, 2, "There are two choice, col2 is empty"); + q1.visibleRows[2].cells[1].value = "CC"; + assert.equal(q2.visibleChoices.length, 3, "There are three choice"); + assert.equal(q2.visibleChoices[2].value, "CC", "the third value is correct"); +}); +QUnit.test("Use carryForward with panel dynamic + choicesFromValueName&choicesFromTextName", function (assert) { + const survey = new SurveyModel({ elements: [ + { type: "paneldynamic", name: "q1", panelCount: 2, + templateElements: [{ name: "q1-q1", type: "text" }, { name: "q1-q2", type: "text" }, { name: "q1-q3", type: "text" }] + }, + { type: "checkbox", name: "q2", choicesFromQuestion: "q1", choicesFromValueName: "q1-q2", choicesFromTextName: "q1-q3" } + ] }); + const q1 = survey.getQuestionByName("q1"); + const q2 = survey.getQuestionByName("q2"); + assert.equal(q2.choicesFromQuestion, "q1", "choicesFromQuestion is set"); + assert.equal(q2.choicesFromValueName, "q1-q2", "choicesFromValueName is set"); + assert.equal(q2.choicesFromTextName, "q1-q3", "choicesFromTextName is set"); + assert.equal(q2.isUsingCarryForward, true, "Carryforward flag is set"); + assert.equal(q2.visibleChoices.length, 0, "There is no choices"); + q1.panels[0].getQuestionByName("q1-q1").value = "A"; + assert.equal(q2.visibleChoices.length, 0, "There is no choices, q1-q2 is empty"); + q1.panels[0].getQuestionByName("q1-q2").value = "AA"; + q1.panels[0].getQuestionByName("q1-q3").value = "AA-aa"; + assert.equal(q2.visibleChoices.length, 1, "There is one choice"); + assert.equal(q2.visibleChoices[0].value, "AA", "the first value is correct"); + assert.equal(q2.visibleChoices[0].text, "AA-aa", "the first text is correct"); + q1.panels[1].getQuestionByName("q1-q1").value = "B"; + q1.panels[1].getQuestionByName("q1-q2").value = "BB"; + q1.panels[1].getQuestionByName("q1-q3").value = "BB-bb"; + assert.equal(q2.visibleChoices.length, 2, "There are two choice"); + assert.equal(q2.visibleChoices[1].value, "BB", "the second value is correct"); + assert.equal(q2.visibleChoices[1].text, "BB-bb", "the second text is correct"); + q1.addPanel(); + assert.equal(q2.visibleChoices.length, 2, "There are two choice, new panel is empty"); + q1.panels[2].getQuestionByName("q1-q1").value = "C"; + assert.equal(q2.visibleChoices.length, 2, "There are two choice, q1-q2 is empty"); + q1.panels[2].getQuestionByName("q1-q2").value = "CC"; + q1.panels[2].getQuestionByName("q1-q3").value = "CC-cc"; + assert.equal(q2.visibleChoices.length, 3, "There are three choice"); + assert.equal(q2.visibleChoices[2].value, "CC", "the third value is correct"); + assert.equal(q2.visibleChoices[2].text, "CC-cc", "the third text is correct"); +}); +QUnit.test("Check isUsingCarryForward on deleting matrix dynamic question", function (assert) { + const survey = new SurveyModel(); + survey.setDesignMode(true); + survey.fromJSON({ elements: [ + { type: "matrixdynamic", name: "q1" }, + { type: "dropdown", name: "q2", choicesFromQuestion: "q1" } + ] }); + const q1 = survey.getQuestionByName("q1"); + const q2 = survey.getQuestionByName("q2"); + assert.equal(q2.choicesFromQuestion, "q1", "set correctly"); + assert.equal(q2.isUsingCarryForward, true, "Carryforward flag is set"); + q1.delete(); + assert.notOk(q2.choicesFromQuestion, "it is empty"); + assert.equal(q2.isUsingCarryForward, false, "Carryforward flag is unset"); +});