Documentation Docs
Documentation Docs

Create Composite Question Types

This help topic describes how to combine multiple questions into a composite question type.

Configure a Composite Question Type

Composite questions are containers for other questions. They are useful when a group of questions has a specific logic that users should not change. End users cannot customize the nested questions directly. However, they can customize the composite question.

The following code configures a Full Name composite question that contains the First Name and Last Name Text questions:

import { ComponentCollection } from "survey-core";

ComponentCollection.Instance.add({
  // A unique name; must use lowercase
  name: "fullname", 
  // A display name used in the Toolbox
  title: "Full Name",
  // A default title for questions created with this question type
  defaultQuestionTitle: "Enter your full name:",
  // An array of JSON schemas that configure the nested questions
  elementsJSON: [
    { type: "text", name: "firstName", title: "First Name", isRequired: true },
    { type: "text", name: "lastName", title: "Last Name", isRequired: true, startWithNewLine: false }
  ]
});

The Full Name composite question in the survey JSON schema looks as follows:

{
  "type": "fullname",
  "name": "question1"
}

A composite question produces an object for a value:

{
  "question1": {
    "firstName": "John",
    "lastName": "Doe"
  }
}

View Demo

Add Custom Properties to Composite Question Types

If you need to control nested questions, add a custom property to their composite questions. Users can change custom properties in the Property Grid.

For example, the Full Name composite question from the previous topic may include an optional Middle Name question. The following code adds a custom showMiddleName property that controls the Middle Name question visibility:

import { ComponentCollection, Serializer } from "survey-core";

ComponentCollection.Instance.add({
  name: "fullname", 
  title: "Full Name", 
  defaultQuestionTitle: "Enter your full name:",
  elementsJSON: [
    { type: "text", name: "firstName", title: "First Name", isRequired: true },
    // Optional question, hidden by default
    { type: "text", name: "middleName", title: "Middle Name", startWithNewLine: false, visible: false },
    { type: "text", name: "lastName", title: "Last Name", isRequired: true, startWithNewLine: false }
  ],

  onInit() {
    // Add a `showMiddleName` Boolean property to the `fullname` question type
    Serializer.addProperty("fullname", {
      name: "showMiddleName",
      type: "boolean",
      default: false,
      category: "general",
    });
  },
  // Set the Middle Name question visibility at startup
  onLoaded(question) {
    this.changeMiddleNameVisibility(question);
  },
  // Track the changes of the `showMiddleName` property
  onPropertyChanged(question, propertyName, newValue) {
    if (propertyName === "showMiddleName") {
      this.changeMiddleNameVisibility(question);
    }
  },
  changeMiddleNameVisibility(question) {
    const middleName = question.contentPanel.getQuestionByName("middleName");
    if (!!middleName) {
      // Set the `middleName` question's visibility based on the composite question's `showMiddleName` property 
      middleName.visible = question.showMiddleName;
    }
  },
});

In the survey JSON schema, the custom showMiddleName property looks as follows:

{
  "type": "fullname",
  "name": "question1",
  "showMiddleName": "true"
}

The steps below summarize how to add a custom property to your composite question:

  1. Implement the onInit function to add a custom property to your question.
  2. Implement a function that connects your custom property with a nested question's property (changeMiddleNameVisibility in the code above).
  3. Call this function from the onLoaded function to apply the custom property when the survey JSON schema is loaded.
  4. Call the same function from the onPropertyChanged function to reapply the custom property each time its value changes.

View Demo

Expressions and Triggers in Composite Question Types

Expressions and triggers help you build conditional logic in a survey. Let us consider a survey example in which respondents should enter their business and shipping addresses. For the case when the addresses are the same, the survey has a "Shipping address same as business address" question that displays a Yes/No toggle switch. When the switch is set to Yes, the Shipping Address field is disabled and its value is copied from the Business Address field:

Composite question type - Shipping Address

When respondents set the switch to No, Shipping Address becomes enabled and its value is cleared:

Composite question type - Shipping Address

The following code shows how you can use expressions and triggers to implement this logic in code:

{
  "elements": [
    {
     "type": "comment",
     "name": "businessAddress",
     "title": "Business Address",
     "isRequired": "true"
    },
    {
     "type": "boolean",
     "name": "shippingSameAsBusiness",
     "title": "Shipping address same as business address",
     "defaultValue": "true"
    },
    {
     "type": "comment",
     "name": "shippingAddress",
     "title": "Shipping Address",
     "enableIf": "{shippingSameAsBusiness} <> true",
     "isRequired": "true"
    }
  ],
  "triggers": [
    {
      "type": "copyvalue",
      "expression": "{shippingSameAsBusiness} = true and {businessAddress} notempty",
      "setToName": "shippingAddress",
      "fromName": "businessAddress"
    },
    {
      "type": "setvalue",
      "expression": "{shippingSameAsBusiness} = false",
      "setToName": "shippingAddress"
    }
  ]
}

Survey Creator users can implement the same UI and logic, but this requires time and basic understanding of expressions and triggers. To help the users with this task, you can create a custom composite question type that already implements this UI and logic. The code below demonstrates this composite question type configuration. Note that the enableIf expression uses the composite prefix to access a nested question. Instead of triggers, composite questions use the onValueChanged function to implement the trigger logic.

import { ComponentCollection } from "survey-core";

ComponentCollection.Instance.add({
  name: "shippingaddress",
  title: "Shipping Address",
  elementsJSON: [{
    type: "comment",
    name: "businessAddress",
    title: "Business Address",
    isRequired: true
  }, {
    type: "boolean",
    name: "shippingSameAsBusiness",
    title: "Shipping address same as business address",
    defaultValue: true
  }, {
    type: "comment",
    name: "shippingAddress",
    title: "Shipping Address",
    // Use the `composite` prefix to access a question nested in the composite question
    enableIf: "{composite.shippingSameAsBusiness} <> true",
    isRequired: true
  }],
  onValueChanged(question, name) {
    const businessAddress = question.contentPanel.getQuestionByName("businessAddress");
    const shippingAddress = question.contentPanel.getQuestionByName("shippingAddress");
    const shippingSameAsBusiness = question.contentPanel.getQuestionByName("shippingSameAsBusiness");

    if (name === "businessAddress") {
      // If "Shipping address same as business address" is selected
      if (shippingSameAsBusiness.value == true) {
        // Copy the Business Address value to Shipping Address
        shippingAddress.value = businessAddress.value;
      }
    }
    if (name === "shippingSameAsBusiness") {
      // If "Shipping address same as business address" is selected, copy the Business Address to Shipping Address
      // Otherwise, clear the Shipping Address value
      shippingAddress.value = shippingSameAsBusiness.value == true ? businessAddress.value : "";
    }
  }
});

Users can add a custom question to their survey like they add a built-in question, and they can use it without any knowledge of expressions and triggers. The resulting survey JSON schema looks as follows:

{
  "type": "shippingaddress",
  "name": "question1"
}

View Demo

Override Base Question Properties

Composite questions inherit properties from their base question type (Question). To override a base property, add it to your composite question and specify a new configuration for it. Call the addProperty(questionType, propertySettings) method on the Serializer object. You can call it in the onInit function within the composite question configuration object.

The following code shows how to override base question properties. This code hides the properties from the Property Grid (visible: false) and excludes them from serialization to JSON. The titleLocation property also gets a new default value.

import { ComponentCollection, Serializer } from "survey-core";

ComponentCollection.Instance.add({
  name: "shippingaddress",
  // ...
  onInit() {
    Serializer.addProperty("shippingaddress", {
      name: "titleLocation",
      visible: false,
      default: "hidden",
    });
    Serializer.addProperty("shippingaddress", {
      name: "title",
      visible: false,
    });
    Serializer.addProperty("shippingaddress", {
      name: "description",
      visible: false,
    });
  }
});

You can also override the properties of the objects nested in a composite question. For example, the following code overrides the showQuestionNumbers property of the Panel that contains all the other nested questions:

ComponentCollection.Instance.add({
  name: "shippingaddress",
  // ...
  onCreated(question) {
    question.contentPanel.showQuestionNumbers = "default";
    // ...
  }
});

If you want to override a nested question property, call the Panel's getQuestionByName method to access the question:

ComponentCollection.Instance.add({
  name: "shippingaddress",
  // ...
  onCreated(question) {
    const businessAddress = question.contentPanel.getQuestionByName("businessAddress");
    businessAddress.visible = false;
  }
});

Localize Composite Questions

You can localize composite questions by following the same technique used to localize survey contents. The following code shows how to translate texts within a composite question to French and German while using English as the default language:

import { ComponentCollection } from "survey-core";

ComponentCollection.Instance.add({
  name: "fullname", 
  title: {
    "default": "Full Name",
    "fr": "Nom et prénom",
    "de": "Vollständiger Name"
  },
  defaultQuestionTitle: {
    "default": "Enter your full name:",
    "fr": "Entrez votre nom complet:",
    "de": "Geben Sie Ihren vollständigen Namen ein:"
  },
  elementsJSON: [
    {
      type: "text",
      name: "firstName",
      title: {
        "default": "First Name",
        "fr": "Prénom",
        "de": "Vorname"
      },
      isRequired: true
    },
    {
      type: "text",
      name: "lastName",
      title: {
        "default": "Last Name",
        "fr": "Nom de famille",
        "de": "Nachname"
      },
      isRequired: true,
      startWithNewLine: false
    }
  ]
});

Further Reading

Send feedback to the SurveyJS team

Need help? Visit our support page

Copyright © 2024 Devsoft Baltic OÜ. All rights reserved.

Your cookie settings

We use cookies on our site to make your browsing experience more convenient and personal. In some cases, they are essential to making the site work properly. By clicking "Accept All", you consent to the use of all cookies in accordance with our Terms of Use & Privacy Statement. However, you may visit "Cookie settings" to provide a controlled consent.

Your renewal subscription expires soon.

Since the license is perpetual, you will still have permanent access to the product versions released within the first 12 month of the original purchase date.

If you wish to continue receiving technical support from our Help Desk specialists and maintain access to the latest product updates, make sure to renew your subscription by clicking the "Renew" button below.

Your renewal subscription has expired.

Since the license is perpetual, you will still have permanent access to the product versions released within the first 12 month of the original purchase date.

If you wish to continue receiving technical support from our Help Desk specialists and maintain access to the latest product updates, make sure to renew your subscription by clicking the "Renew" button below.