SurveyJS Architecture
This article will help you acquire a better understanding of how SurveyJS products are designed and why you can easily customize and integrate them with any backend.
SurveyJS Products
SurveyJS is a set of client-side libraries that enable you to build a full-cycle form management system on your in-house infrastructure. SurveyJS libraries use JSON objects to communicate with the server, and therefore you can integrate our libraries with any backend.
SurveyJS product line consists of the following products:
- SurveyJS Form Library - Collects survey responses.
- Survey Creator - Allows users to design surveys in a no-code UI.
- SurveyJS Dashboard - Visualizes survey results and allows you to analyze them.
- SurveyJS PDF Generator - Saves surveys as PDF documents.
SurveyJS Form Library is free; the other products require a commercial license.
You can use SurveyJS products in any React, Angular, Vue, Knockout, or jQuery application. React, Angular, Knockout, Vue 3, and Vue 2 (Form Library only) are supported natively; jQuery and Survey Creator for Vue 2 are supported via a wrapper over the Knockout version.
SurveyJS Packages
SurveyJS is distributed as npm packages and as scripts and style sheets that you can reference on your page. In this section, we use the word "package" to refer to both these means of distribution.
SurveyJS Core Package
SurveyJS architecture separates business logic from rendering. This gives us the following benefits:
- Business logic is shared across all libraries, not duplicated.
- Business logic can be unit-tested without running resource-intensive rendering tests.
- Native rendering implementation for each individual client-side framework is possible.
Business logic is encapsulated within the survey-core
package. It contains no rendering capabilities and does not depend on any JavaScript framework. This package implements the following parts:
- Survey data model
- Question types
- Question containers (pages and panels)
- Expression support
- Data validation functions
- Themes and styles
Since the survey-core
package is a dependency of all other SurveyJS packages, it is installed automatically when you install one of those packages. However, in some cases, you may need to install and use survey-core
as a standalone package. For example, your web server may run NodeJS and you may want to validate user responses on it, as shown in the following code snippet:
import { Model } from "survey-core";
const survey = new Model(surveyJson);
survey.data = responseData; // Assign a response to the survey
survey.validate(); // Validate the response against the survey JSON schema
Survey Rendering Packages
Survey rendering packages contain rendering primitives that allow SurveyJS Form Library to natively support each individual JavaScript framework. All rendering packages depend on survey-core
and framework packages. SurveyJS Form Library ships with an individual package for each supported framework:
survey-react-ui
survey-angular-ui
survey-vue-ui
survey-vue3-ui
survey-knockout-ui
(used with Knockout and jQuery)
Survey Creator Packages
Much like SurveyJS Form Library, Survey Creator code is also split between a framework-independent core package and several framework-specific rendering packages that depend on the core. All the packages are listed below:
survey-creator-core
survey-creator-react
survey-creator-angular
survey-creator-vue
survey-creator-knockout
(used with Knockout, jQuery, and Vue 2)
Many Survey Creator elements are in fact heavily stylized surveys from our own Form Library. This allows you to use the same technique to customize Survey Creator elements as you would use to customize regular surveys.
Other Survey Creator elements are also inherited from the Form Library. For example, a drop-down menu with built-in search is a Dropdown survey question, and the adaptive action bar is a survey navigation bar.
SurveyJS Dashboard Package
SurveyJS Dashboard code is distributed as the survey-analytics
package. This library uses Survey Model from the survey-core
package and an array of survey responses in JSON format to create a dashboard with charts, tables, and other data visualization elements. SurveyJS Dashboard is built upon Plotly.js.
SurveyJS PDF Generator Package
SurveyJS PDF Generator code is distributed as the survey-pdf
package. You can use PDF Generator to create fillable PDF forms based on a survey JSON schema. This package depends upon Survey Model from the survey-core
package and requires the jsPDF library to generate PDF.
Survey Data Model
A survey data model describes the survey structure, content, and layout. Topics in this section explain how survey data models work.
Serialization and Deserialization
To create a survey data model, you can define a JSON object. For example, the following code specifies a survey data model that contains one Checkboxes question with three answer options and the capability for respondents to suggest their own answer:
const json = {
elements: [{
type: "checkbox",
name: "programmingLanguages",
title: "What programming languages do you use?",
choices: [
{ value: "c++", text: "C++"},
{ value: "js", text: "JavaScript" },
{ value: "java", text: "Java" }
],
showOtherItem: true
}]
};
A JSON object is the easiest way to create a survey data model. You can also save JSON objects to a database and restore them to display your surveys on another device.
Alternatively, a survey data model can be created in JavaScript code. This approach is useful if you want to configure a data model dynamically. The following code creates the same data model without using JSON:
const survey = new Model();
const page = survey.addNewPage();
const question = page.addNewQuestion("checkbox");
question.name = "programmingLanguages";
question.title = "What programming languages do you use?";
question.choices.push(new ItemValue("c++", "C++"));
question.choices.push(new ItemValue("js", "JavaScript"));
question.choices.push(new ItemValue("java", "Java"));
question.showOtherItem = true;
Every SurveyJS object (survey, page, question) can be serialized (converted to JSON suitable for storage) and deserialized (converted to a JavaScript object) using the toJSON()
and fromJSON()
API methods. These methods allow you to combine JSON and JavaScript code when you create a survey model. In the following example, the question contents are declared in a JSON object, but the question type is specified in JavaScript code:
const questionJson = {
name: "programmingLanguages",
title: "What programming languages do you use?",
choices: [
{ value: "c++", text: "C++" },
{ value: "js", text: "JavaScript" },
{ value: "java", text: "Java" }
],
showOtherItem: true
};
const survey = new Model();
const page = survey.addNewPage();
const question = page.addNewQuestion("checkbox");
question.fromJSON(questionJson);
SurveyJS runs serialization and deserialization with the help of the Serializer singleton class. This class's API allows you to specify or customize (de)serialization rules. For example, the addClass(name, propMeta[], constructor, baseClassName)
method defines (de)serialization rules for an entire SurveyJS object. In the following code, this method registers (de)serialization rules for three properties of a "radiogroup"
SurveyJS object. If a "radiogroup"
object already exists, the method overrides rules only for these three properties.
import { Serializer } from "survey-core";
Serializer.addClass(
"radiogroup",
// `radiogroup` properties and their (de)serialization rules
[
{ name: "showClearButton:boolean", default: false },
{ name: "separateSpecialChoices", visible: true },
{ name: "itemComponent", visible: false, default: "survey-radiogroup-item" }
],
// A constructor used to instantiate the `radiogroup` object in JavaScript code
function () {
return new QuestionRadiogroupModel("");
},
// An object from which `radiogroup` should inherit other properties
"checkboxbase"
);
Another Serializer method, addProperty(className, propMeta)
, allows you to add a custom property to an existing SurveyJS object. It works similarly to addClass()
, but it also adds the new property to a list of custom properties. After SurveyJS class instantiation, the Serializer gets all custom properties for this class and its parents and checks whether each property exists in the created instance. If a property does not exist, the Serializer uses the Object.defineProperty
method to create it. As a result, custom properties behave like built-in properties. The following code adds a custom property tag
to the "question"
object. This property will be displayed in the General section in the Survey Creator's Property Grid.
import { Serializer } from "survey-core";
Serializer.addProperty("question",
{ name: "tag", category: "general" }
);
Custom properties cannot be included in SurveyJS TypeScript declaration files and recognized by TS compiler. To access custom properties in TS, use the following syntax: (<any>question).tag
or question["tag"]
.
Specialized and Composite Question Types
In SurveyJS, the functionalities of built-in question types cover a wide range of use cases. This makes the question types flexible but at the same time generic. If you need a question type with more tailored functionality, you can create a specialized question type. For example, you can create a Dropdown question with a pre-configured set of answer options. Survey Creator users will not be able to change the options and thus break the functionality. Refer to the following help topic for more information: Create Specialized Question Types.
Another distinct variation of custom question types is a composite question. It is a container that nests other questions. You can configure data validation, conditional logic, and other properties for the nested questions, and Survey Creator users will only need to drag and drop the composite question onto the design surface to have the configured functionality in their survey. If you change the conditional logic or add a new question to the composite question in the future, you will only need to modify your JavaScript code, without editing numerous survey JSON definition. For more details on composite questions, refer to the following article: Create Composite Question Types.
Custom Question Types
In addition to specialized and composite questions, which are based upon built-in question types, you can create custom question types that render any content. The list below outlines the steps you need to follow to implement a custom question type:
Create a Model
You can start by extending an existing question model class. Add necessary properties and methods and test them in your unit testing framework.Configure Serialization
Use theaddClass()
method to specify serialization rules.Register the Question in
ElementFactory
TheElementFactory
singleton has an API to work with question types. Call theregisterElement
method to register your custom question.Implement a Rendering Class
While built-in question types include rendering classes for all supported frameworks, you need to implement rendering only for the framework you use.
Since the same instructions apply to the process of third-party component integration, refer to the following materials more information on custom question types:
- Integrate Third-Party React Components
- Integrate Third-Party Angular Components
- Integrate Third-Party Vue 3 Components
Events
SurveyJS Form Library raises over 80 events, while for Survey Creator, this number is close to 50. All these events enable you to significantly alter the behavior of SurveyJS components.
In SurveyJS, an event is not a callback function but rather a class that has the following methods:
add(func)
- Attaches a callback function as a handler to an event.remove(func)
- Detaches a callback function from an event.clear()
- Detaches all handlers from an event.fire()
- Executes all attached functions.
A callback function that handles an event receives two parameters: sender
and options
. sender
is a SurveyJS object that raises the event. Typically, it is a survey model or Survey Creator model. options
is a JavaScript object that includes properties and methods useful for this event. This object allows you to handle an event and modify the behavior of the SurveyJS component.
The following code example shows how to handle events on the example of SurveyModel's onComplete
event:
survey.onComplete.add((sender, options) => {
console.log(JSON.stringify(sender.data));
});
Expressions and Conditional Logic
SurveyJS expressions help you implement the following behavior:
- Show and hide, enable and disable, or make a question required based on a condition.
- Perform an action (complete the survey, jump to another question) based on the expression result.
- Calculate values and display them.
- Calculate values and store them in survey results.
Refer to the following help topic for more information on each of these use cases: Conditional Logic and Dynamic Texts.
The SurveyJS expression engine is built upon the PEG.js parser generator. The engine can perform arithmetic and logical operations, supports built-in functions (iif
, currentDate
, min
, max
, etc.), and allows you to implement custom functions. In addition, the engine is optimized for maximum performance as follows:
- An expression is re-evaluated only when the values of questions used in it are changed.
- The Boolean decision tree is cached.
- Paths to questions and survey values are also cached to access them without repeated parsing.
Grammar rules used to generate the SurveyJS expression parser are uploaded to the survey-library GitHub repository.
You can build expressions of any complexity. For example, the following expression calculates a total sum by adding VAT to the total price and applying a 10% discount if the price is over 1000. total
and vat
are question names; iif
is a built-in function.
({total} * (1 + {vat}/100) * iif({total} > 1000, 0.9, 1)
Many properties accept expressions as their values. You can implement a custom expression property. In the following example, the showHeaderIf
property displays the matrix header only if an expression assigned to this property evaluates to true
:
import { Serializer } from "survey-core";
Serializer.addProperty("matrix", {
name: "showHeaderIf:condition", category: "logic",
onExecuteExpression: (obj, res) => {
obj.showHeader = res === true;
}
});
You can replace the SurveyJS expression engine with your own if necessary. This may be useful if you are migrating to SurveyJS from another form management platform but want to keep the old expression syntax for any reason. To do this, you need to implement the IExpressionExecutor
interface.
Reactivity
Although the survey data model and the UI rendering part are separated, the UI reacts to changes in the model. SurveyJS implements this behavior in a manner described below.
Model property values are stored in a dictionary. To get or set them, each property calls the getPropertyValue(propName, defaultValue?)
and setPropertyValue(propName, value)
methods in its getter and setter. These methods are inherited from the Base class.
export class QuestionModel extends Base {
get propertyName() {
return this.getPropertyValue("propertyName");
}
set propertyName(val) {
this.setPropertyValue("propertyName", val);
}
}
The setPropertyValue(propName, value)
method raises an event that we handle differently depending on the framework: call setState
in React, create observable properties in Knockout, etc. This approach allows us to separate the business model from the UI while retaining reactivity.
If you use TypeScript, you can apply the @property
decorator to a property declaration instead of the getter-setter pair to define a reactive model property. Let's change the code above so that it uses the @property
decorator:
import { property } from "survey-core";
export class QuestionModel extends Base {
@property() propertyName: string;
}
Localization and Multi-Language Support
The UI of SurveyJS products is translated into over 30 languages. Predefined translations are shipped as dictionary files. Each file contains a JSON object with translation strings for one language. You can add new strings to a dictionary file or use it as a base to create dictionaries for other languages.
In code, each translation string is represented by an instance of the LocalizableString
class. This instance contains its own inner dictionary with translations of that particular string for different locales. The inner dictionary has values only if you override the predefined translations. Otherwise, the inner dictionary is empty, and the LocalizableString
instance takes translations from dictionary files.
Each survey or Survey Creator instance includes the locale
property. You can change it based on the browser's locale to translate the survey or Survey Creator UI for the current region. If a string for a specified locale is not found, the component searches for the translation in a generic locale (for example, "pt" for "pt-BR" and "pt-PT"). If that search doesn't give a result either, then the string from the default English locale is used. You can also specify a different locale to be used as default.
The localization engine is distributed as a separate module. Refer to the following help topics for more information on how to enable and configure it:
Replaceable UI Components
Survey and Survey Creator UI is built from components that can be replaced with other components. For example, the design survey in Survey Creator renders different components for an element in focus and default states. When you click the title of a question, panel, or page, you can edit the title text. To implement this functionality, Survey Creator substitutes the default component with a component that renders an editable input field.
You can use the same approach to enhance and customize the UI of your surveys. Refer to the demos below for code examples. Note that these demos include different customization code because UI components are created differently in each JavaScript framework.