Tweet |
Lightning Web Components and Flows
Introduction
This week (8th May 2020, for anyone reading this in a different week!) saw the Salesforce Low Code Love online event, showcasing low code tools and customer stories. This was very timely for me, as I'd finally found some time to try out embedding Lightning Web Components in Lightning Flows.
I don't spend a lot of time writing flows - not because I don't want to, but because it's not particularly good use of my time, which is better spent on architecture, design and writing JavaScript or Apex code. Some developers don't like working on flows, and I suspect there are a couple of reasons:
- It slows them down - they find that expressing complex logic in a visual tool requires a lot of small steps that could be expressed far more efficiently in code. In this case I'd suggest that a step backwards is required to decide if flow is the best tool to write that particular piece of logic in - which is not saying the whole flow should be discarded, but maybe this aspect should be treated as a micro-service and moved to another technology. In much the same way that Evergreen will allow us to move processing this is better suited to another platform outside of Salesforce - dip out of flow for the complex work that is unlikely to change, leaving the simpler steps that need regular tweaking.
- They can't be unit tested in isolation. This is probably my biggest peeve - if I write some Apex code to automate a business process, I can unit test it with a multitude of different inputs and configuration, and easily repeat those tests when I make a change. While I can include auto-launched flows in my unit test, they are mixed in with the other automation that is present and so may experience side effects, while screen flows are entirely manual so I'd need to use a tool like ProVar to create automated UI tests. I get that it's tricky, as screen flows span multiple transactions, but it feels like something that needs to be solved if low-code tools are to gain real currency with IT departments.
The old maxim that the further away from the developer you find a bug, the more expensive it is to fix still holds true.
Scenario
The first thing I should say is that Lightning Web Components can only be embedded in screen flows, so for headless flows you'll be using Apex actions. In my example my web component simply displays a message and automatically navigates without any user interaction, so would be a candidate for an Apex action, but I quite like the idea of being able to interact with the user if I need to, and I was also curious if there were any issues with my approach.Drawing on my Building my own Learning System work, I decided to implement a simple quiz where the user is presented with a number of questions that need to be answered via a radio button, for single answer, or checkbox group, where multiple selections are required. Once the user has made their selection(s), this is compared with the correct answers and their score updated accordingly. Once all questions have been answered, the user is told how they got on.
The Flow
Building the flow was very straightforward to begin with - getting the questions from the database, looping through them, updating a counter so I could show the question and a screen to ask the question, with conditionally rendered inputs depending on the question type:There was some behind the scenes work with variables to create the choices for the radiobutton/checkbox groups, but nothing untoward. When marking the answers however, things got a lot more tricky. I had to retrieve and iterate the answers, identify the type of the user's selection (which could be the radio button or checkbox output from the screen), check the answer against their selection and then update their score appropriately. There's probably a few minor variations on this, but expressing all of those steps visually ended up with quite a complex looking flow:
Once you have multiple decisions inside a loop, the sheer number of connectors makes it difficult to lay out the flow nicely, and adding custom boolean logic to a decision quickly gets ugly - in this case I'm checking if the user failed on this answer by not choosing an answer that is correct or choosing an answer that is incorrect:
One of the advantages of building functionality in flow is that it is easier to change, but understanding what is going on here and making changes would not be simple and there would be a fair amount of retesting required. This kind of thing is the worst of both worlds - it slows development, limits the capabilities but isn't easy to rework.
With this many small moving parts to carry out some processing that is highly unlikely to change that often, flow seems like the wrong tool to create the marking functionality in. I could move some of it into a sub-flow, but that feels like trying to hide how much the flow is doing rather than genuine functional decomposition.
Embedded Lightning Component
So I decided to replace the marking aspect with a Lightning Web Component - this will take the question and the various inputs that a user can supply and figure out whether they answered correctly. It will return true or false into an output variable.To embed a Lightning Web Component in a flow, it needs to specify lightning__FlowScreen as a target in the -meta.xml file:
<targets> <target>lightning__FlowScreen</target> </targets>
and to interact with the flow via input/output variables, these must be declared in the target config stanza for the lightning__flowScreen target :
<targetConfigs> <targetConfig targets="lightning__FlowScreen"> <property name="questionId" type="String" /> <property name="radioChosen" type="String" /> <property name="selectChosen" type="String" /> <property name="correct" type="Boolean" /> </targetConfig> </targetConfigs>I have three input properties :
- questionId - the id of the question that has been answered
- radioChosen - the selected value for a radio button question
- selectChosen - the selected value for a checkbox group question
And one output property:
- correct - did the user answer the question correctly
As well as detailing the properties in the metadata file, I need appropriate public functions in the component JavaScript class. For input properties I need a public getter:
@api get questionId() { return this._questionId; }
while for output properties I need a public setter:
@api set correct(value) { this._correct=value; }
I also need to fire an event to tell flow that the value of the output parameter has changed, which I'll cover in a moment.
When my input properties are set, I need to mark the question, but I have no control over the order that they are set in. I'm marking via an Apex method so I don't want to call that when I've only received the question id. Each of my setters calls the function to mark the question, but before doing anything this checks that I've received the question id and one of the checkbox or radio button option sets:
markQuestion() { if ( (this._questionId) && (this._radioChosen || this._selectChosen) ) { MarkQuestionApex({questionId: this._questionId, answers: (this._radioChosen?this._radioChosen:this._selectChosen)})This invokes the following Apex method
@AuraEnabled public static Boolean MarkQuestion(String questionId, String answers) { Question__c question=[select id, (select id, Name, Correct__c from Answers__r) from Question__c where id=:questionId]; Boolean correct=true; for (Answer__c answer : question.Answers__r) { if ( ((!answer.Correct__c) && (answers.contains(answer.Name))) || ((answer.Correct__c) && (!answers.contains(answer.Name))) ) { correct=false; } } return correct; }Now I know I'm biased as I like writing code, but this seems a lot easier to understand - small things like retrieving the question and it's related answers in a single operation. writing boolean login in a single expression rather than combining conditions defined elsewhere make a big difference. I can also write unit tests for this method and give it a thorough workout before committing any changes.
My function from my Lightning Web Component receives the result via a promise and fires the event to tell flow that the value of the correct property has been changed:
MarkQuestionApex({questionId: this._questionId, answers: (this._radioChosen?this._radioChosen:this._selectChosen)}) .then(result => { const attributeChangeEvent = new FlowAttributeChangeEvent('correct', result); this.dispatchEvent(attributeChangeEvent); const navigateNextEvent = new FlowNavigationNextEvent(); this.dispatchEvent(navigateNextEvent); })
It also fires the event to navigate to the next event (the next question or the done screen) as I don't need anything from the user.
I add this to my flow and wire up the properties via a screen component - my markQuestion component appears in the custom list, and I specify the inputs and outputs as I would for any other flow component:
Embedding this in my flow simplifies things quite a bit:
Now any admin coming to customise this flow can easily see what is going on - the question marking that doesn't change is encapsulated in a single screen component and they can easily add more screens - to display the running total for example, or cut the test short if they user gets too many questions wrong. Note that as this is another screen, the user will be taken to it when they click 'Next' - I display a 'Marking question' message, but if you don't want this then an Apex Action would be a better choice.
You can find the metadata components at my Low Code Github repository, but if you want to see the two flows in action, here's a video:
Great post, Bob. Your conclusion is very aligned with what we (Salesforce Automation group) are telling developers these days: the highest and best use of Flow is not to get you to stop coding. It's to enable you to publish your code as declarative building blocks that can be easily customized by the large ranks of non-coders that are out there. Note that with Summer '20, developer like yourself will also be able to put custom property editors on those lwcs, giving them state of the art GUI configuration (see https://unofficialsf.com/preview-quick-choice-2-0-with-a-custom-property-editor/ for more)
ReplyDelete