One of the really elegant parts of Lightning Components was the ability to conditionally apply classes based on data.
This is something that is no longer available to us, as the expressions we are allowed to put into templates are now limited to either:
- A property of the Javascript class (or a sub-property that can be referenced through a top level property).
- A getter, that accepts no parameters.
I’ve already mentioned a change in this area in this blog post about building re-usable components, but I felt it was time to drill into this just a little further...
Update - 28/12/2018
As seems to be increasingly the case, as soon as I posted this I was corrected on a couple of points. I've since updated the post to include that feedback. Hopefully the result isn't too confusing!The scenario
Let’s say we want to render a list of objects. There’s a flag 'isSelected' on each of the records, and if that flag is set we want to change the rendering for that particular record.
JSON data:
[ { "id" : 1 , "name" : "Anne" , "isSelected" : false }, { "id" : 2 , "name" : "Bob" , "isSelected" : true }, { "id" : 3 , "name" : "Carla" , "isSelected" : true } ]
Required Output:
<ul> <li class="record">Anne</li> <li class="selected record">Bob</li> <li class="selected record">Carla</li> </ul>
Lightning Component
In a Lightning Component, this would be near trivial, as we could use a ternary operator in the template to render the inclusion of the 'selected' class conditionally.
<ul> <aura:iteration items="{!v.records}" var="thisRecord"> <li class="{!(thisRecord.isSelected?'selected':'' ) + ' record' }">{!thisRecord.name} </aura> </ul>
The reason this is so simple, is that we could put proper expressions into our replacements in Lightning Components, giving us fantastic flexibility in the output for each individual attribute.
Unfortunately (for this case), this isn't possible in Lightning Web Components...
Lightning Web Component
First up, let's just clarify what we mean when we say we can no longer do the string concatenation, or the ternary operator in an attribute expression, as I detailed in my earlier post.
What we mean is, we can’t do the following:
<ul> <template for:each={records} for:item="thisRecord"> <li class="{thisRecord.isSelected?'selected':'' + ' record' }">{thisRecord.name}</li> </template> </ul>
All we can do is reference a single value from our data held against our Javascript object, or call a getter against it. E.g. (not that this template is of much use to us right now)
<ul> <template for:each={records} for:item="thisRecord"> <li class={thisRecord.isSelected}>{thisRecord.name}</li> </template> </ul>
OK - so what other options do we have?
Option 1 - Build your class lists in your data:
So, we could build up the list of classes that we want to render against each record in our data - once the data is populated from where-ever, we can loop over the records and update the data so we end up with something like:
JSON data:[ { "id" : 1 , "name" : "Anne" , "isSelected" : false , "classes" : "record" }, { "id" : 2 , "name" : "Bob" , "isSelected" : true , "classes" : "selected record" }, { "id" : 3 , "name" : "Carla" , "isSelected" : true , "classes" : "selected record" } ]
We can then render the required output like this:
<ul> <template for:each={records} for:item="thisRecord"> <li key={thisRecord.id} class={thisRecord.classes}>{thisRecord.name}</li> </template> </ul>
Pros:
- The template is simple.
- Since we're processing in Javascript, we can draw on any information and make whatever complex rules we want.
Cons:
- We need to process the data after we’ve built it. This means we can no longer use a wired property, and need to use either a wired function or imperatively called Apex. This may not be a big deal, but it's worth noting.
- Data retrieved from Apex is immutable, so if this is the only thing we need to add to the data, then may find that we need to copy data into new objects, or add a new data structure in order to get the classes property added - that can be a pain.
- The logic for the classes that each record should have assigned is held in Javascript. I recognise that this is the direction we seem to be going, but I still like to avoid this where possible.
Option 2 - Use a template 'if' and repeat the li tag.
If we want to avoid doing anything complex in our Javascript, we can add template 'if's into the markup, and conditionally render the <li> tag in its two different forms.
For example, we could do the following:
<ul> <template for:each={records} for:item="thisRecord"> <template if:true={thisRecord.isSelected}> <li key={thisRecord.id} class="selected record">{thisRecord.name}</li> </template> <template if:false={thisRecord.isSelected}> <li key={thisRecord.id} class="record">{thisRecord.name}</li> </template> </template> </ul>
Pros:
- The Javascript doesn't contain any of the logic for the conditional rendering.
Cons:
- We're breaking the "Don't repeat yourself" (DRY) principle, and repeating the structure of the <li> tag in each side of the IF condition. In this simple case this may not seem like a big deal, but still - any change to that rendering now needs to be made in 2 places, instead of 1. And let's be honest, how often is the case this simple? We'll probably find that we have to copy a LOT of the template to work like this.
Option 3 - Use a template 'if', and change our CSS.
Another alternative is to use the template if, but to isolate the part that changes from the part that doesn't. That is, we introduce HTML inside our 'if:true' that only exists in order to apply the formatting that should be assigned to the 'isSelected' records.
That is, we do the following in our template, to introduce a new, standalone, div that has the 'selected' class applied, and then wrap the content of the <li> in another div.
<ul> <template for:each={records} for:item="thisRecord"> <li key={thisRecord.id} class="record"> <template if:true={thisRecord.isSelected}> <div class="selected"></div> </template> <div>{thisRecord.name}</div> </li> </ul>
Having done this, we can use more advanced CSS selectors to apply our 'selected' style to the div that follows the div with 'selected' as its class.
For example, let's say our 'selected' records should have a green border:
.selected+div { border: 1px solid green; }
The selector '.selected+div' means 'The div that follows the tag with the class 'selected'.
You can read about CSS Selectors here.
Pros:
- We conditionally render only the addition of the class in the template - nothing is repeated.
- The Javascript doesn't contain any of the logic for the conditional rendering.
Cons:
- We need to introduce additional structure into the HTML that exists purely to apply classes to other elements. This isn't ideal and can change the behaviour of other classes further down the structure (e.g. we have introduced a div here - what impact does that have?)
Option 4 - Introduce a sub component.
It would be good if we could call a getter function at run time in order to get the list of classes, along the lines of the component described in the earlier post.
The problem is that we can't call a function and pass it the context of the individual record that we are rendering.
So does that mean we can't call a function?
No, it just means that we need to narrow the context of the component down into each record before we call the function - and we can do that with a sub-component that just renders the <li>.
We can call our sub-component (recordRenderer) with something like this:
<ul> <template for:each={records} for:item="thisRecord"> <c-record-renderer key={thisRecord.id} record={thisRecord}></c-record-renderer> <template> </ul>
Our sub-component template can be:
<li class={classes}>{record.name}</li>
And our sub-component javascript can be:
import { LightningElement, api } from 'lwc'; export default class RecordRenderer extends LightningElement { @api record; get classes() { if ( this.record.isSelected ) { return 'selected record'; } return 'record' } }
Pros:
- Both the template for the parent and sub component are very simple and focused on small aspects of the rendering - no IFs in the templates is a good thing IMO.
- It's very possible that there will be more complex behaviour required in the future, and having the context set to be the individual record could make that behaviour much simpler to implement in the future. For example, passing data into events due to a 'click' event could be well served if we have that distinction between the parent and child components and context.
Cons:
- OK, we have the classes logic inside the Javascript, much like in Option 1, but we don't have to process the data - the getter is called at render time. And, presumably the code in the renderer is near trivial, so maybe that's not such a big problem.
- Update - 28/12/2018 - Unfortunately though, in this case, we generated non-valid HTML. Since we have introduced a new component, we have also introduced a new tag into our generated HTML. The result is something along the lines of ul -> c-record-renderer -> li, and this is not strictly valid. We should certainly bear this in mind when looking at individual situations.
Conclusions
Update 28/12/2018
The conclusions were re-written after the feedback that you can find in the comments below, and some more thought on the topic.The limitations of the expressions allowed in templates makes for a less elegant solution to this kind of problem. However, that's not an altogether terrible thing. Sometimes you can have too much power and too many choices. Limitations can allow us to define recognised best-practices that are harder to break away from.
Even with those limitations, there are many different ways in which we can implement any required functionality, and even this simple case has 4 options of unequal merit
So which way should we go?
Well, that depends on the precise scenario, and in this case we have defined something precise, but potentially a little too simplistic to define a clear best-practice.
I would find it hard to argue that Option 2 (repetition in the template) is ever worth the pain - repeating chunks of template or code is rarely a good idea, even in the simplest of cases.
Option 1 (pre-process the data) has its merits, and as Caridy has responded below, is the recommended approach from Salesforce - that's not to be sniffed at. However, if the only reason you would be processing the data is to add a list of classes to it, I'd suggest that it shouldn't be your immediate 'go-to' solution.
My initial reaction to Option 3 (advanced CSS selectors) was that it was a decent one with few drawbacks, although the more I look at it, the more I dislike the fact that we are breaking the semantics of the HTML document we are generating. We introduce structure who's only purpose is to define the rendering behaviour of another component. That's not really how HTML is supposed to work. Ideally we have meaning in our structure, and this structure expresses no meaning.
So, even though Option 3 may often be straightforward, I'd probably shy away from it now. Just because CSS selectors can be used in this way, doesn't mean that they should.
Option 4 (decompose the component) doesn't really work in this case - it generates non-valid HTML. But that doesn't mean it never works. There will be plenty of similar scenarios where the resulting HTML is perfectly valid. In fact, I'm sure we'll often find scenarios where the sub-component we produce will be a re-usable component that we can drop into other scenarios easily. In that situation we'll surely want to decompose instead?
So where does that leave us?
Simply put - there's no clear answer, as is generally the case - the technique we use should be appropriate for the scenario we're implementing - though options 2 and 3 will rarely be appropriate..
- If there are other good reasons to split some of the rendering into a sub-component, then splitting it out is probably a good idea - but if that results in non-valid HTML, then it's a bad idea.
- If there are other reasons to pre-process and re-shape the data, then adding the class definition to that processing is probably a good idea - but if it forces you to clone records and re-shape them just to add a class, then maybe it's not the best idea.
I definitely take the point from Salesforce that the latter is their preferred route, but my natural instinct is pretty much always to reduce processing and make smaller things. It'll be interesting to see how much my opinion changes as I build real applications with Lightning Web Components. Based on how much my opinion has changed in the last 24 hours - I expect it to change a lot!