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