Building dynamic forms with SurveyJs and Blazor WebAssembly

Introduction

In this blog, we will learn how to integrate SurveyJs in the .NET Core 6.0 Blazor Web Assembly application to create dynamic forms. SurveyJs saves both questions and answers in JSON format. So it will be easier to manipulate the data via SQL, C#, or any JavaScript framework, according to your business needs. Since SurveyJs is completely based on JavaScript, we will use the concept of Js Interop in Blazor to pass the data between the Blazor component and JavaScript.

Please follow the official documentation of SurveyJs for more information.

Prerequisites

  • Visual Studio 2022 installed,
  • Microsoft SQL Server 18,
  • Basics of Asp .Net Web, Entity Framework

Source Code

This source code is publicly available on GitHub link.

Step 1. Open Visual Studio 2022 Click on Create a New Project, and choose a Blazor WebAssembly.application. Make sure you have checked the ASP.NET Core hosted checkbox while creating the project.

SurveyJS with Blazor

It will create a client, server, and shared projects like this.

Project

Step 2. Set the SurveyJsWithBlazor.Server project as a startup project and then run. Now you will see the sample Blazor application that has been created.

How blazor working

Step 3. We will need the dynamic form builder and consumer for the complete implementation of the dynamic form. For that, we will add two Navigation menu item for the Dynamic Form creator and Dynamic Form consumer in SurveyJsWithBlazor.Client/Shared/NavMenu.razor page.

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="creator">
                <span class="oi oi-plus" aria-hidden="true"></span> Dymanic form Creator   
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="consumer">
                <span class="oi oi-plus" aria-hidden="true"></span> Dymanic form Consumer
            </NavLink>
        </div>

Let’s add two razor component files for the builder and consumer inside the pages folder.

Solution explorer folder

Now we have to add the following surveyJs reference in the index.html file.

<script src="https://unpkg.com/knockout/build/output/knockout-latest.js"></script>
<script src="https://unpkg.com/[email protected]/survey.core.min.js"></script>
<script src="https://unpkg.com/[email protected]/survey.i18n.min.js"></script>
<script src="https://unpkg.com/[email protected]/themes/index.min.js"></script>
<script src="https://unpkg.com/[email protected]/survey-knockout-ui.min.js"></script>
<script src="https://unpkg.com/[email protected]/survey-creator-core.min.js"></script>
<script src="https://unpkg.com/[email protected]/survey-creator-core.i18n.min.js"></script>
<script src="https://unpkg.com/[email protected]/themes/index.min.js"></script>
<script src="https://unpkg.com/[email protected]/survey-creator-knockout.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/defaultV2.css" />
<link rel="stylesheet" href="https://unpkg.com/[email protected]/survey-creator-core.css" />

Step 4. Inside wwwroot folder add two js files: (a) builder.js (b) consumer.js.

Inside builder.js paste the following code.

var SurveyHelper = SurveyHelper || {};
SurveyHelper.ShowSurvey = function (dotNetObject, creatorText) {
    
    var creatorOptions = {
        showLogicTab: false,
        showJSONEditorTab: false
    };
    var creator = new SurveyCreator.SurveyCreator(creatorOptions);
    creator.render("creatorElement");
    creator.showToolbox = "right";
    creator.showPropertyGrid = "right";
    creator.rightContainerActiveItem("toolbox");

    //Automatically save survey definition on changing. Hide "Save" button
    //creator.isAutoSave = true;
    //Show state button here
    //creator.showState = true;

    //Setting this callback will make visible the "Save" button
    creator.saveSurveyFunc = function (saveNo, callback) {
        //save the survey JSON
        //console.log(creator.text);
        //You can store in your database JSON as text: creator.text  or as JSON: creator.JSON

        dotNetObject.invokeMethodAsync('SaveSurvey', creator.text);

        //We assume that we can't get error on saving data in local storage
        //Tells creator that changing (saveNo) saved successfully.
        //Creator will update the status from Saving to saved
        callback(saveNo, true);
    }

    var defaultJSON = {
        pages: [
            {
                name: 'page1',
                elements: [
                    {
                        type: 'text',
                        name: "Your First Question.."
                    }
                ]
            }
        ]
    };
    if (!creatorText)
        creatorText = JSON.stringify(defaultJSON);
    creator.text = creatorText;

    //If you get JSON from your database then you can use creator.JSON property
    //creator.JSON = yourJSON;
};

It will define a ShowSurvey function. Inside that function survey creator object has been created. Various properties can be enabled or disabled to customize the SurveyJs UI. We will be using the Js Interop to Call the JavaScript function from Blazor.

Inside consumer.js paste the following code.

var AssessmentHelper = AssessmentHelper || {};

var json = {};
var assessmentObj = {};

AssessmentHelper.ShowAssessment = function (dotNetObject, surveyQuestion, assessment) {

    if (surveyQuestion)
        json = JSON.parse(surveyQuestion);
    if (assessment)
        assessmentObj = JSON.parse(assessment);

    const survey = new Survey.Model(json);

    //Load the initial state
    survey.data = assessmentObj;

    ko.applyBindings({
        model: survey
    }, document.getElementById("surveyElement"));

    survey
        .onComplete
        .add(function (sender, callback) {

            dotNetObject.invokeMethodAsync('SaveAssessment', JSON.stringify(sender.data));

            callback(sender, true);
        });
}

This code will show the dynamic component that has been configured by the builder and the user can fill out the form and then submit it. So submitted form content will be passed to the saveAssessment function of SurveyJsConsumer.razor page.

Let’s add two JavaScript file references to the index.html page.

<script src="builder.js"></script>
<script src="consumer.js"></script>

Now the index.html file will look like this.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>SurveyJsWithBlazor</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="SurveyJsWithBlazor.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">๐Ÿ—™</a>
    </div>

    <script src="_framework/blazor.webassembly.js"></script>

    <script src="https://unpkg.com/knockout/build/output/knockout-latest.js"></script>
    <script src="https://unpkg.com/[email protected]/survey.core.min.js"></script>
    <script src="https://unpkg.com/[email protected]/survey.i18n.min.js"></script>
    <script src="https://unpkg.com/[email protected]/themes/index.min.js"></script>
    <script src="https://unpkg.com/[email protected]/survey-knockout-ui.min.js"></script>
    <script src="https://unpkg.com/[email protected]/survey-creator-core.min.js"></script>
    <script src="https://unpkg.com/[email protected]/survey-creator-core.i18n.min.js"></script>
    <script src="https://unpkg.com/[email protected]/themes/index.min.js"></script>
    <script src="https://unpkg.com/[email protected]/survey-creator-knockout.min.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/defaultV2.css" />
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/survey-creator-core.css" />

    <script src="builder.js"></script>
    <script src="consumer.js"></script>
</body>

</html>

Step 5. In your surveyJsBuilder.razor page paste the following code.

@page "/creator"
@inject HttpClient Http
@inject IJSRuntime Jsr
@implements IDisposable

<h4>Compose your Dynamic Form here..</h4>

<div id="surveyContainer">
    <div id="creatorElement"></div>
</div>

@code {

    [Inject]
    public IJSRuntime jsRuntime { get; set; }
    private IJSObjectReference? module;
    DotNetObjectReference<SurveyJsBuilder> ObjectReference;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ObjectReference = DotNetObjectReference.Create(this);
            await Jsr.InvokeVoidAsync("SurveyHelper.ShowSurvey", ObjectReference, "");
        }
    }

    // being called by js, check dependency while changes
    [JSInvokable("SaveSurvey")]
    public async Task SaveQuestions(string surveyContent)
    {
        // place your code here.. to make an API call and save the surveyJson.
        await Jsr.InvokeVoidAsync("alert", "Survey Builder: " + surveyContent);

    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);

        if (ObjectReference != null)
        {
            //Now dispose our object reference so our component can be garbage collected
            ObjectReference.Dispose();
        }
    }
}

Paste the below code in the SurveyJsConsumer.razor.

@page "/consumer"

@inject HttpClient _httpClient
@inject IJSRuntime jsRuntime
@inject HttpClient Http
@implements IDisposable
@inject NavigationManager NavigationManager

<h4> Please fill your answer.</h4>

<div id="surveyElement">
    <survey params="survey: model"></survey>
</div>

@code {
    [Parameter]
    public string Id { get; set; }

    bool isLoading = true;
    private IJSObjectReference? module;
    string builderJson = "";
    DotNetObjectReference<SurveyJsConsumer> ObjectReference;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ObjectReference = DotNetObjectReference.Create(this);

            // make an api call to get the questionJson, for now let's use some static json..
            builderJson = "{\"pages\":[{\"name\":\"page1\",\"elements\":[{\"type\":\"text\",\"name\":\"YourFirstQuestion..\",\"title\":\"What is your Name?\"},{\"type\":\"dropdown\",\"name\":\"question1\",\"title\":\"What is your Gender?\",\"choices\":[{\"value\":\"Item1\",\"text\":\"Male\"},{\"value\":\"Item2\",\"text\":\"Female\"},{\"value\":\"Item3\",\"text\":\"Other\"}]}]}]}";
            await jsRuntime.InvokeVoidAsync("AssessmentHelper.ShowAssessment", ObjectReference, builderJson, "");
            StateHasChanged();
        }
    }

    // being called by js, check dependency while changes
    [JSInvokable("SaveAssessment")]
    public async Task SaveAssessment(string surveyContent)
    {
        // your code to make an API call and save the answer.
        await jsRuntime.InvokeVoidAsync("alert", "Your Answer in Json Format: "+surveyContent);
        
    }
    public void Dispose()
    {
        GC.SuppressFinalize(this);

        if (ObjectReference != null)
        {
            //Now dispose our object reference so our component can be garbage collected
            ObjectReference.Dispose();
        }
    }
}

Run the application and enjoy your dynamic form application in Blazor Web Assembly.

Compose dynamic form

At form builder, you can add 20 types of questions and various validations (required field, conditional validation, logical validation, etc.)

The consumer page is designed for the end user who fills out the form.

Cheers!