A deep dive into my Salesforce development experience: Build (2/3)
My first post talked about my design & planning for a component to help display & update Salesforce data according to a defined business process. This one is the next step - looking into how I developed it.
Starting with a high fidelity wireframe made developing the component’s logic a bit more structured. Even when prototyping, I had a rough idea of what the backend code needed to do, but going back to my initial notes helped me focus on a couple areas:
SAFE HARBOUR: There's still probably a lot here that isn't 'good' dev practices. I'm learning!
ADMIN CONFIGURATION
When writing code for virtually anything, maintainability is a critical consideration: from code comments, to alignment to common patterns & frameworks, to the documentation itself - all very important! Something I don’t see as often is thought about how an admin is going to use & extend the functionality once it has been delivered. To be fair, often the requirements given to developers are simply to ‘make the system do this thing’, but the more configurability a component has, the more reuse it should have. Maybe even this relates to less developer time needed when the system needs enhancements, and dare I say, maybe even a lower total cost of ownership? I shouldn't get ahead of myself..
As I knew that the component was going to be highly flexible, I landed on the following attributes needing to be configurable:
My initial proof of concept used a single custom metadata object record to contain all configuration, but I figured that in order for the component to seamlessly transition from displaying one group of fields for StageValue1 to another group of fields for StageValue2, I landed on a two-tiered approach:
You’ll see that the parent-level details are stored in the parent custom metadata type (CMDT) record - these values are static. Child-level details e.g. XYZ fields to display when the field is in ABC status are kept in a child-level CMDT record, and this helps the component easily determine what should be shown when. The admin can define what parent-level CMDT record is used when they add the component to the page in the Lightning App Builder menu.
USER INTERFACE BUILD
As I mentioned in the previous post, at this point I already had done quite a bit of the design. Additionally, the LWC framework gives you quite a few tools to accelerate common tasks like form creation. I’ve learned that any sort of development on Salesforce should start with investigating the tools the platform gives you, and then rolling your own if they truly are not fit for purpose.
By using lightning-record-edit-form for my screen input, I had started with a lot of the work already done for me - I was actually quite surprised at how quickly it took to get to this point. I didn’t have to worry as much about data security & visibility, or if a certain type of user was able to update one field but another user was not. Very convenient!
I also had to determine how to display the completion percentage of each record. Determining completion is quite difficult: sure, a required field being blank would suggest it is incomplete, but what about a field that isn’t formatted the correct way, or is dependent on the value of another? Ultimately, because this logic could be very complex, I took a very simple approach instead, by giving the task to an admin instead. The admin can define a field (e.g. a formula field) with a value from 0-100 that is updated according to whatever logic they like, and the component will simply display that value. Or, they can ignore it completely and nothing is displayed.
Something I learned early on was that using the lightning-progress-ring, while pretty, was somewhat difficult to work with. It is small unless you play around with CSS, was difficult to position ideally, and was surprisingly distracting especially if you had multiple records open. I decided to replace it with lightning-process-indicator, which has worked well to convey the same information.
QUERYING THE BACKEND
Once the custom metadata was sorted out, I had to figure out the sequence in which I query data to build up the user interface. Ultimately, the Apex controller for the component looks like this:
Recommended by LinkedIn
I used a wrapper to make returning data to the LWC more simple - instead of individual wires returning 4 objects, I could just do one, right? This almost worked.
This approach (returning all data in one wire) had me passing the 2 parameters I’d need to ultimately complete all 4 queries above - the record Id and the parent CMDT name, and return one big object that I could process in the javascript. To my delight, when I started testing this - it was displaying data like I expected! However, it wasn’t reactive. When I changed the record’s stage from New to In Progress, the component wouldn’t change the fields to update until the user refreshed the page.
MANAGING WIRES & REACTIVITY
This was a problem for me. Technically it wasn’t something I’d written in my acceptance criteria, but there was a clear usability problem. What followed was me, begrudgingly, rewriting a lot of what I had done. I didn't want to do this, but I had to do this.
A clear first issue was that I had deep-cloned the recordData object (returned via my wire), in order to mutate it and make passing dynamic labels etc. to my child component a bit easier. Obviously, deep-cloning the object prevented any reactive Lightning Data Service functionality from happening, so that got reworked.
I then looked into refreshing the wire-cached data and tried to use refreshApex() and getRecordNotifyChange(), driven by a custom event from the child to force the parent to re-query everything. This addressed a minor issue - the parent component displays the name of each queried child record in the accordion. If the user updated the name of the child record, what was displayed on the parent was the old name, but by using the event & refreshApex, this was now being kept in sync.
My major one issue was still unsolved: I needed the component to be reactive when the user changes the record’s stage OUTSIDE of the component! This ‘reactivity’ is well-supported by the LWC framework and the underlying Lightning Data Service (LDS), but unfortunately, not my brain. What I ultimately landed on was:
What resulted was the desired functionality - when a user changes a stage/value on the parent record, the getRecord wire fires and calls my Apex methods, returning the new fields to be displayed*. I also was able to use this approach instead of the refreshApex method above to keep the display of parent/child data in sync between parent/child components.
* As I wrote that, I realised I could probably have just returned all child CMDT records from my Apex controller and then determined the correct fields to show via logic in the javascript, which would then mean I don’t need to requery the CMDT to determine what fields need to be shown. Oops?
Not that it's changed much since last week, but this is what the 'finished' product looks like:
THINGS THAT AREN’T PERFECT
While I’m comfortable enough with what I’ve done so far, and it does work, it definitely isn’t 100% finished:
I could probably address the first three items here, but in all honesty, I’m kind of pooped at this point. Maybe when I pick up some more motivation I’ll circle back to these.
You can see all the source code for it here.
WRAPPING UP
Ultimately, I’ve glossed over a lot of the heartache that went into landing on the ‘finished product’. I spent easily a day or so playing around with ways to refresh my cached data, and I’m sure a big part of that was simply me not understanding things well enough.
There was quite a lot I learned along the way - both on the design/planning and development side of things. Watch out for my final little article, presumably coming soon, about the key learnings I’ve taken away from this process!