A deep dive into my Salesforce development experience: Build (2/3)

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:

  • Admin configurability
  • User interface
  • Querying for data & metadata

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:

  • What object’s records would be displayed (i.e. child records/line items)
  • What relationship to use to link parent and child records (e.g. OpportunityId)
  • What the controlling field is (i.e. the API name of the ‘stage field’ e.g. StageName)
  • What fields would be displayed on the child records (i.e. Name, Role)
  • What the controlling field’s value should be to display these fields (e.g. only display Name and Role when StageName = ‘Negotiation’)

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:

No alt text provided for this image

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.

No alt text provided for this image


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:

  1. Get the parent CMDT record which has the same name as what was set in the App Builder menu
  2. Get the record the user is currently viewing via the passed in recordId parameter (this gets the current value of the ‘stage’ record)
  3. Get the child CMDT record, based off the parent & the current value of the stage record
  4. Get all child records as defined in the parent CMDT, with the fields as defined by the child CMDT - after the previous 3 queries to prepare, this is your query to actually get record data

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:

  • Removing my wired functions that directly called an Apex method (as I was having problems with data caching), and turning these into imperative calls.
  • Instead, adding the getRecord wire to listen for changes to the parent record. This doesn’t actually query for data itself (see line 48 here), but it will imperatively hit the same Apex methods.

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:

No alt text provided for this image


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 have done very little to catch exceptions. There’s a really obvious issue where if you change the record’s stage to a value that doesn’t have a corresponding CMDT record, the component hangs and a user can’t interact with the page at all.
  • The SOQL queries should enforce security - lightning-record-edit-form does help me with this anyway, but it should be consistent across the component.
  • There’s no test class, so technically this can’t be deployed to production.
  • An issue that came up quite late in development was that I have to use an ugly workaround to display data from Service Cloud objects correctly. This is because we cannot use dynamic expressions in LWC markup, and I can’t mutate the data returned from my wires if I want the component to be reactive, so it’s difficult to determine the ‘name’ to display for child record data. Technically this means that an admin may have to write a line of code to use this component :(

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!

To view or add a comment, sign in

More articles by Josh Dennis

Insights from the community

Others also viewed

Explore topics