diff --git a/src/jsonobject.ts b/src/jsonobject.ts index 06c9d91b51..0085870074 100644 --- a/src/jsonobject.ts +++ b/src/jsonobject.ts @@ -1051,42 +1051,58 @@ export class JsonMetadata { } public getPropertiesByObj(obj: any): Array { if (!obj || !obj.getType) return []; - var res: any = {}; - var props = this.getProperties(obj.getType()); - for (var i = 0; i < props.length; i++) { - res[props[i].name] = props[i]; - } - var dynamicProps = !!obj.getDynamicType - ? this.getProperties(obj.getDynamicType()) - : null; - if (dynamicProps && dynamicProps.length > 0) { - for (var i = 0; i < dynamicProps.length; i++) { - let dProp = dynamicProps[i]; - if (!!res[dProp.name]) continue; - res[dProp.name] = dProp; + const props = this.getProperties(obj.getType()); + const dynamicProps = this.getDynamicPropertiesByObj(obj); + return [].concat(props).concat(dynamicProps); + } + public addDynamicPropertiesIntoObj(dest: any, src: any, props: Array): void { + props.forEach(prop => { + this.addDynamicPropertyIntoObj(dest, src, prop.name, false); + if (prop.serializationProperty) { + this.addDynamicPropertyIntoObj(dest, src, prop.serializationProperty, true); + } + if (prop.alternativeName) { + this.addDynamicPropertyIntoObj(dest, src, prop.alternativeName, false); } + }); + } + private addDynamicPropertyIntoObj(dest: any, src: any, propName: string, isReadOnly: boolean): void { + var desc = { + configurable: true, + get: function () { + return src[propName]; + }, + }; + if (!isReadOnly) { + (desc)["set"] = function (v: any) { + src[propName] = v; + }; } - return Object.keys(res).map((key) => res[key]); + Object.defineProperty(dest, propName, desc); } public getDynamicPropertiesByObj(obj: any, dynamicType: string = null): Array { - if (!obj || !obj.getType || (!obj.getDynamicType && !dynamicType)) - return []; - const objType = obj.getType(); + if (!obj || !obj.getType) return []; + if(!!obj.getDynamicProperties) return obj.getDynamicProperties(); + if(!obj.getDynamicType && !dynamicType) return []; const dType = !!dynamicType ? dynamicType : obj.getDynamicType(); - if (!dType) return []; - const cacheType = dType + "-" + objType; + return this.getDynamicPropertiesByTypes(obj.getType(), dType); + } + public getDynamicPropertiesByTypes(objType: string, dynamicType: string, invalidNames?: Array): Array { + if (!dynamicType) return []; + const cacheType = dynamicType + "-" + objType; if(this.dynamicPropsCache[cacheType]) return this.dynamicPropsCache[cacheType]; - var dynamicProps = this.getProperties(dType); + var dynamicProps = this.getProperties(dynamicType); if (!dynamicProps || dynamicProps.length == 0) return []; - var hash: any = {}; - var props = this.getProperties(objType); + const hash: any = {}; + const props = this.getProperties(objType); for (var i = 0; i < props.length; i++) { hash[props[i].name] = props[i]; } - var res = []; - for (var i = 0; i < dynamicProps.length; i++) { - let dProp = dynamicProps[i]; - if (!hash[dProp.name]) { + const res = []; + if(!invalidNames) invalidNames = []; + for (let i = 0; i < dynamicProps.length; i++) { + const dProp = dynamicProps[i]; + if (!hash[dProp.name] && invalidNames.indexOf(dProp.name) < 0) { res.push(dProp); } } @@ -1630,23 +1646,18 @@ export class JsonObject { private addDynamicProperties( obj: any, jsonObj: any, - properties: Array + props: Array ): Array { - if (!obj.getDynamicPropertyName) return properties; - var dynamicPropName = obj.getDynamicPropertyName(); - if (!dynamicPropName) return properties; - if (jsonObj[dynamicPropName]) { - obj[dynamicPropName] = jsonObj[dynamicPropName]; - } - var dynamicProperties = this.getDynamicProperties(obj); - var res = []; - for (var i = 0; i < properties.length; i++) { - res.push(properties[i]); - } - for (var i = 0; i < dynamicProperties.length; i++) { - res.push(dynamicProperties[i]); + if (!obj.getDynamicPropertyName && !obj.getDynamicProperties) return props; + if(obj.getDynamicPropertyName) { + const dynamicPropName = obj.getDynamicPropertyName(); + if (!dynamicPropName) return props; + if (dynamicPropName && jsonObj[dynamicPropName]) { + obj[dynamicPropName] = jsonObj[dynamicPropName]; + } } - return res; + const dynamicProps = this.getDynamicProperties(obj); + return dynamicProps.length === 0 ? props : [].concat(props).concat(dynamicProps); } private propertiesToJson( obj: any, diff --git a/src/question_custom.ts b/src/question_custom.ts index 622db43a84..f6f3a232c3 100644 --- a/src/question_custom.ts +++ b/src/question_custom.ts @@ -1,5 +1,5 @@ import { Question, IConditionObject } from "./question"; -import { Serializer, CustomPropertiesCollection } from "./jsonobject"; +import { Serializer, CustomPropertiesCollection, JsonObjectProperty } from "./jsonobject"; import { ISurveyImpl, ISurveyData, @@ -82,6 +82,7 @@ export interface ICustomQuestionTypeConfiguration { * ``` */ defaultQuestionTitle?: any; + inheritBaseProps?: false | true | Array; /** * A function that is called when the custom question is created. Use it to access questions nested within a [composite question type](https://surveyjs.io/form-library/documentation/customize-question-types/create-composite-question-types). * @@ -239,6 +240,7 @@ export interface ICustomQuestionTypeConfiguration { } export class ComponentQuestionJSON { + private dynamicProperties: Array; public constructor(public name: string, public json: ICustomQuestionTypeConfiguration) { var self = this; Serializer.addClass( @@ -331,6 +333,34 @@ export class ComponentQuestionJSON { public get isComposite(): boolean { return !!this.json.elementsJSON || !!this.json.createElements; } + public getDynamicProperties(): Array { + if(!Array.isArray(this.dynamicProperties)) { + + } + this.dynamicProperties = this.calcDynamicProperties(); + return this.dynamicProperties; + } + private calcDynamicProperties(): Array { + const baseProps = this.json.inheritBaseProps; + if(!baseProps || !this.json.questionJSON) return []; + const type = this.json.questionJSON.type; + if(!type) return []; + if(Array.isArray(baseProps)) { + const props: Array = []; + baseProps.forEach(name => { + const prop = Serializer.findProperty(type, name); + if(prop) { + props.push(prop); + } + }); + return props; + } + const invalidNames = []; + for(let key in this.json.questionJSON) { + invalidNames.push(key); + } + return Serializer.getDynamicPropertiesByTypes(this.name, type, invalidNames); + } } export class ComponentCollection { @@ -681,8 +711,15 @@ export class QuestionCustomModel extends QuestionCustomModelBase { public getTemplate(): string { return "custom"; } - protected createWrapper() { + public getDynamicProperties(): Array { + return this.customQuestion.getDynamicProperties() || []; + } + public getDynamicType(): string { + return this.questionWrapper ? this.questionWrapper.getType() : "question"; + } + protected createWrapper(): void { this.questionWrapper = this.createQuestion(); + this.createDynamicProperties(this.questionWrapper); } protected getElement(): SurveyElement { return this.contentQuestion; @@ -842,6 +879,13 @@ export class QuestionCustomModel extends QuestionCustomModelBase { } this.isSettingValueChanged = false; } + private createDynamicProperties(el: SurveyElement): void { + if(!el) return; + const props = this.getDynamicProperties(); + if(Array.isArray(props)) { + Serializer.addDynamicPropertiesIntoObj(this, el, props); + } + } protected initElement(el: SurveyElement): void { super.initElement(el); if (!!el) { diff --git a/src/question_matrixdropdowncolumn.ts b/src/question_matrixdropdowncolumn.ts index b55987bc3e..16fdc7b45a 100644 --- a/src/question_matrixdropdowncolumn.ts +++ b/src/question_matrixdropdowncolumn.ts @@ -838,36 +838,8 @@ export class MatrixDropdownColumn extends Base } } private addProperties(curCellType: string) { - var question = this.templateQuestion; - var properties = this.getProperties(curCellType); - for (var i = 0; i < properties.length; i++) { - var prop = properties[i]; - this.addProperty(question, prop.name, false); - if (prop.serializationProperty) { - this.addProperty(question, prop.serializationProperty, true); - } - if (prop.alternativeName) { - this.addProperty(question, prop.alternativeName, false); - } - } - } - private addProperty( - question: Question, - propName: string, - isReadOnly: boolean - ) { - var desc = { - configurable: true, - get: function () { - return (question)[propName]; - }, - }; - if (!isReadOnly) { - (desc)["set"] = function (v: any) { - (question)[propName] = v; - }; - } - Object.defineProperty(this, propName, desc); + const props = this.getProperties(curCellType); + Serializer.addDynamicPropertiesIntoObj(this, this.templateQuestion, props); } } diff --git a/tests/question_customtests.ts b/tests/question_customtests.ts index 84e7f47899..5bb3de1eea 100644 --- a/tests/question_customtests.ts +++ b/tests/question_customtests.ts @@ -2730,3 +2730,86 @@ QUnit.test("composite component: defaultQuestionTitle", function (assert) { ComponentCollection.Instance.clear(); }); +QUnit.test("single component: inheritBaseProps: array", function (assert) { + ComponentCollection.Instance.add({ + name: "customdropdown", + inheritBaseProps: ["allowClear", "showOtherItem"], + questionJSON: { + type: "dropdown", + choices: [1, 2, 3] + }, + }); + + const survey = new SurveyModel({ + elements: [ + { type: "customdropdown", name: "q1", allowClear: false, showOtherItem: true } + ] + }); + const q1 = survey.getQuestionByName("q1"); + const content = q1.contentQuestion; + assert.equal(q1.allowClear, false, "q1.allowClear #1"); + assert.equal(content.allowClear, false, "content.allowClear #1"); + q1.allowClear = true; + assert.equal(q1.allowClear, true, "q1.allowClear #2"); + assert.equal(content.allowClear, true, "content.allowClear #2"); + content.allowClear = false; + assert.equal(q1.allowClear, false, "q1.allowClear #3"); + assert.equal(content.allowClear, false, "content.allowClear #3"); + + assert.equal(q1.showOtherItem, true, "q1.showOtherItem #1"); + assert.equal(content.showOtherItem, true, "content.showOtherItem #1"); + q1.showOtherItem = false; + assert.equal(q1.showOtherItem, false, "q1.showOtherItem #2"); + assert.equal(content.showOtherItem, false, "content.showOtherItem #2"); + content.showOtherItem = true; + assert.equal(q1.showOtherItem, true, "q1.showOtherItem #3"); + assert.equal(content.showOtherItem, true, "content.showOtherItem #3"); + const json = q1.toJSON(); + assert.equal(json.allowClear, false, "json.allowClear"); + assert.equal(json.showOtherItem, true, "json.showOtherItem"); + + ComponentCollection.Instance.clear(); +}); +QUnit.test("single component: inheritBaseProps: true", function (assert) { + ComponentCollection.Instance.add({ + name: "customdropdown", + inheritBaseProps: true, + questionJSON: { + type: "dropdown", + choices: [1, 2, 3] + }, + }); + + const survey = new SurveyModel({ + elements: [ + { type: "customdropdown", name: "q1", allowClear: false, showOtherItem: true } + ] + }); + const q1 = survey.getQuestionByName("q1"); + const content = q1.contentQuestion; + assert.equal(q1.getDynamicType(), "dropdown", "q1.getDynamicType()"); + assert.equal(content.choices.length, 3, "content.choices"); + assert.notOk(q1.choices, "q1.choices"); + assert.equal(q1.allowClear, false, "q1.allowClear #1"); + assert.equal(content.allowClear, false, "content.allowClear #1"); + q1.allowClear = true; + assert.equal(q1.allowClear, true, "q1.allowClear #2"); + assert.equal(content.allowClear, true, "content.allowClear #2"); + content.allowClear = false; + assert.equal(q1.allowClear, false, "q1.allowClear #3"); + assert.equal(content.allowClear, false, "content.allowClear #3"); + + assert.equal(q1.showOtherItem, true, "q1.showOtherItem #1"); + assert.equal(content.showOtherItem, true, "content.showOtherItem #1"); + q1.showOtherItem = false; + assert.equal(q1.showOtherItem, false, "q1.showOtherItem #2"); + assert.equal(content.showOtherItem, false, "content.showOtherItem #2"); + content.showOtherItem = true; + assert.equal(q1.showOtherItem, true, "q1.showOtherItem #3"); + assert.equal(content.showOtherItem, true, "content.showOtherItem #3"); + const json = q1.toJSON(); + assert.equal(json.allowClear, false, "json.allowClear"); + assert.equal(json.showOtherItem, true, "json.showOtherItem"); + + ComponentCollection.Instance.clear(); +});