Friday 13 March 2020

Creating - Lightning Dynamic Question & Answer Form


Requirement

Need to display a Question & Answer form based upon input received from 3rd party web service i.e. web service response will tell us 
  • What Question text to be displayed
  • Answer type might be of Single/Multiple Choice/Free Text based (to keep it simple, I have not included other types like Date, Numeric etc)
  • Some of questions are not mandatory to be answered

End of the implementation we can see slimier UI as displayed below:






Sample response (JSON) from 3rd party Service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[
  {
    "questionId": "Q0001",
    "question": "How many tickets do you have?",
    "required": true,
    "questionDisplayType": "PICKLIST",
    "answerChoices": ["1","2","3"]
  },
  {
    "questionId": "Q00002",
    "question": "What is your real name?",
    "required": false,
    "readonly": false,
    "questionDisplayType": "TEXT"
  },
  {
    "questionId": "Q00003",
    "question": "What is your favorite color?",
    "required": true,
    "readonly": false,
    "questionDisplayType": "RADIOGROUP",
    "answerChoices": ["Red","Blue","Yellow","Green"]
  },
  {
    "questionId": "Q00004",
    "question": "Select countries you've visited",
    "required": true,
    "readonly": false,
    "questionDisplayType": "CHECKBOXGROUP",
    "answerChoices": ["India","USA","UK"]
  }
]

Solution Approach:

Will create a reusable component (CommonInputComponent.cmp) which will render/generate required input components based upon display type provided from parent (Q_A_Form.cmp) component. And finally will capture Answers from parent component itself.

Note: Also given code repository link at the button of this post.

Now its code time: 

Code is very self -explanatory but I have explained using component level comments as required.

Child Component: CommonInputComponent.cmp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!--
  @File Name          : CommonInputComponent.cmp
  @Description        : Input type is determined based on the display type provided
  @Author             : Avijit Gorai
  @Group              : 
  @Last Modified By   : Avijit Gorai
  @Last Modified On   : 11/3/2020, 1:12:47 am
  @Modification Log   : 
  Ver       Date            Author            Modification
  1.0    11/3/2020   Avijit Gorai     Initial Version
-->
<aura:component
    implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,forceCommunity:availableForAllPageTypes"
    access="global">

    <!-- Attributes -->
    <aura:attribute name="questionId" type="String" required="true" />
    <aura:attribute name="questionName" type="String" required="true" />
    <aura:attribute name="questionDisplayType" type="String" required="true" />
    <aura:attribute name="picklistOptions" type="Object[]" />
    <aura:attribute name="required" type="Boolean" default="false" />
    <aura:attribute name="disabled" type="Boolean" default="false" />
    <aura:attribute name="readonly" type="Boolean" default="false" />
    <aura:attribute name="fieldMetadata" type="Object" access="private" />
    <aura:attribute name="fieldValue" type="Object" access="public" />
    <aura:attribute name="fieldValueChb" type="List" access="public" />
    <aura:attribute name="answerChoices" type="List" default="[]" />

    <!-- aura method, getting called from parent component to check input validaty and show message accordingly -->
    <aura:method name="checkReportValidity" action="{!c.showReportValidity}" access="public"></aura:method>

    <!-- Handlers -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />

    <!-- TEXT -->
    <aura:if isTrue="{!v.questionDisplayType == 'TEXT'}">
        <lightning:input aura:id="inputField" value="{!v.fieldValue}" label="{!v.questionName}"
            onchange="{!c.handleFieldValueChanged}" maxlength="{!v.fieldMetadata.maxLength}" required="{!v.required}"
            disabled="{!v.readonly}" />
    </aura:if>

    <!-- PICKLIST -->
    <aura:if isTrue="{!v.questionDisplayType == 'PICKLIST'}">
        <lightning:select aura:id="inputField" label="{!v.questionName}" value="{!v.fieldValue}"
            required="{!v.required}" disabled="{!v.readonly}" onchange="{!c.handleFieldValueChanged}">
            <aura:iteration items="{!v.picklistOptions}" var="picklistOption">
                <option text="{!picklistOption.label}" value="{!picklistOption.value}" />
            </aura:iteration>
        </lightning:select>
    </aura:if>

    <!-- RADIOGROUP -->
    <aura:if isTrue="{!v.questionDisplayType == 'RADIOGROUP'}">
        <lightning:radioGroup aura:id="inputField" label="{!v.questionName}" options="{!v.picklistOptions}"
            value="{!v.fieldValue}" type="radio" onchange="{!c.handleFieldValueChanged}" required="{!v.required}"
            disabled="{!v.readonly}" class="customRadioCls" />
    </aura:if>

    <!-- CHECKBOXGROUP -->
    <aura:if isTrue="{!v.questionDisplayType == 'CHECKBOXGROUP'}">
        <lightning:checkboxGroup aura:id="inputField" label="{!v.questionName}" options="{!v.picklistOptions}"
            value="{!v.fieldValueChb}" onchange="{!c.handleFieldValueChanged}" required="{!v.required}"
            disabled="{!v.readonly}" />
    </aura:if>

</aura:component>

CommonInputComponentController.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
({
    //CommonInputComponentController.js
    
    doInit: function (component, event, helper) {
        helper.setFieldMetadata(component, event);
    },
    handleFieldValueChanged: function (component, event, helper) {
        helper.handleFieldValueChanged(component, event);
    },
    //Shows the help message if the form control is in an invalid state.
    showReportValidity: function (component, event, helper) {
        component.find("inputField").showHelpMessageIfInvalid();
    }
});

CommonInputComponentHelper.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
({
    //CommonInputComponentHelper.js
    
    //setting metadata (options to select) 
    setFieldMetadata: function (component, event) {

        var fieldMetadata = new Object();

        fieldMetadata.questionDisplayType = component.get('v.questionDisplayType');

        if (fieldMetadata.questionDisplayType === 'TEXT') {
            fieldMetadata.maxLength = 180;
        }

        if (fieldMetadata.questionDisplayType === 'PICKLIST' ||
            fieldMetadata.questionDisplayType === 'RADIOGROUP' ||
            fieldMetadata.questionDisplayType === 'CHECKBOXGROUP') {
            //console.log('answerChoices -> ' + component.get('v.answerChoices'));
            var answerChoices = component.get('v.answerChoices');
            if (answerChoices) {
                console.log(answerChoices);
                var result = [];
                if (fieldMetadata.questionDisplayType !== 'RADIOGROUP' && fieldMetadata.questionDisplayType !== 'CHECKBOXGROUP') {
                    result.push({label: '', value: ''}); //inserting blank option
                }
                for (var i in answerChoices) {
                    result.push({
                        label: answerChoices[i],
                        value: answerChoices[i]
                    });
                }
                fieldMetadata.picklistOptions = result;
                fieldMetadata.picklistOptions.sort((a, b) => (a.value > b.value) ? 1 : -1); //sorting
                component.set('v.picklistOptions', fieldMetadata.picklistOptions);
            }
        }
        component.set('v.fieldMetadata', fieldMetadata);
    },
 
    //onchange, assigning changed value to attribute 'fieldValue'. 
    //This is helpful to get/access value of the input component from parent component
    handleFieldValueChanged: function (component, event) {
        var newFieldValue = event.getParam('value') !== undefined ? event.getParam('value') : event.getSource().get('v.value');
        component.set('v.fieldValue', newFieldValue);
    }
})

Parent Component: Q_A_Form.cmp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!--
  @File Name          : Q_A_Form.cmp
  @Description        : 
  @Author             : Avijit Gorai
  @Group              : 
  @Last Modified By   : Avijit Gorai
  @Last Modified On   : 13/3/2020, 12:28:51 am
  @Modification Log   : 
  Ver       Date            Author            Modification
  1.0    11/3/2020   Avijit Gorai     Initial Version
-->

<aura:component implements="forceCommunity:availableForAllPageTypes" access="global">
    <aura:attribute name="questionAnswerMap" type="List" default="[]" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />

    <div class="slds-p-around_x-large">
        <!--Rendering CommonInputComponent based upon data received, each of component having aura id = 'fieldId' -->
        <aura:iteration items="{!v.questionAnswerMap}" var="fieldValue">
            <c:CommonInputComponent aura:id="fieldId" questionId="{!fieldValue.questionId}"
                questionName="{!fieldValue.question}" required="{!fieldValue.required}"
                readonly="{!fieldValue.readonly}" questionDisplayType="{!fieldValue.questionDisplayType}"
                answerChoices="{!fieldValue.answerChoices}" />
        </aura:iteration>
        <br /><br />
        <lightning:button onclick="{!c.submit}" label="Submit" iconName="utility:save" iconPosition="left"
            variant="brand" />
    </div>
</aura:component>

Q_A_FormController.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
({
    //Q_A_FormController.js

    doInit: function (component, event, helper) {
        //data received from web service (JSON) - to keep this demo simple, I have omitted Webservice call
        //this is the JSON equivalent data in Object format
        var questions = [];
        var qaData = new Object();
        qaData.questionId = "Q0001";
        qaData.question = "How many tickets do you have?";
        qaData.required = true;
        qaData.questionDisplayType = "PICKLIST";
        qaData.answerChoices = ["1", "2", "3"];
        questions.push(qaData);

        qaData = new Object();
        qaData.questionId = "Q00002";
        qaData.question = "What is your real name?";
        qaData.required = false;
        qaData.readonly = false;
        qaData.questionDisplayType = "TEXT";
        questions.push(qaData);

        qaData = new Object();
        qaData.questionId = "Q00003";
        qaData.question = "What is your favorite color?";
        qaData.required = true;
        qaData.readonly = false;
        qaData.questionDisplayType = "RADIOGROUP";
        qaData.answerChoices = ["Red", "Blue", "Yellow", "Green"];
        questions.push(qaData);

        qaData = new Object();
        qaData.questionId = "Q00004";
        qaData.question = "Select countries you've visited";
        qaData.required = true;
        qaData.readonly = false;
        qaData.questionDisplayType = "CHECKBOXGROUP";
        qaData.answerChoices = ["India", "USA", "UK"];
        questions.push(qaData);
        console.log(questions);

        console.log(JSON.stringify(questions));
        component.set("v.questionAnswerMap", questions);
    },

    submit: function (component, event, helper) {
        var requiredMissing = false;
        //finding list of rendered component based upon aura id = fieldId. This will give us a array of component
        const cmps = component.find("fieldId"); 
        if (!cmps) return;
        //looping through to check if current component's value is required but input value has not been provided
        //then calling checkReportValidity aura method to check its input validaty and show message accordingly
        cmps.forEach(function (cmp) {
            let selectedVal = cmp.get("v.fieldValue");
            console.log(cmp.get("v.questionId") + " -- " + selectedVal);
            if (cmp.get("v.required") && (!selectedVal || selectedVal.length == 0)) {
                requiredMissing = true;
                cmp.checkReportValidity();
                console.log("Required field is missing for " + cmp.get("v.questionId"));
            }
        });

        if (requiredMissing) {
            console.log("requireFieldMissing");
        } else {
            //all fine then collecting input value from each of components and added to a Map for further use as per business need
            let answersMap = new Map();
            cmps.forEach(function (cmp) {
                let fieldValue = cmp.get("v.fieldValue");
                answersMap.set(cmp.get("v.questionId"), fieldValue === undefined ? "" : fieldValue);
            });
            
            console.log("answersMap --> ", answersMap);

            let successMapStr = JSON.stringify(Object.fromEntries(answersMap.entries()));
            helper.showTosteMessage(
                component,
                "",
                "success",
                successMapStr,
                "dismissible"
            );
        }
    }
});

Q_A_FormHelper.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
({
  //Q_A_FormHelper.js

  showTosteMessage: function(component, title, type, message, mode) {
    var toastEvent = $A.get("e.force:showToast");
    if (toastEvent) {
      toastEvent.setParams({
        title: title,
        type: type,
        message: message,
        mode: mode
      });
      toastEvent.fire();
    }
    // if not running in LEX or SF1, toast is not available - use alert
    else {
      alert(title + ": " + message);
    }
  }
});

Code has been kept @ https://github.com/AvijitGoraiGitHub/dynamicUILightning


No comments:

Post a Comment