- Abstract
- Installation
- Development Best Practices
- Tips and Tricks
The repository contains source code and documentation for "Lightning Web Components: Use it now". There are 2 main topics: LWC best practices and tips&tricks. The webinar material doesn't try to be a definitive guide for work with LWC but hopefully it will help you to understand how to work with LWC more effective way.
The presentation can be found here.
Currently there is a simple installation.
- Create a scratch org
- Push the changes via
sfdx force:source:push
- Open any salesforce app. The home page will display main components.
- If the page doesn't show you something different to a standard home page
- Open org in Lightning, then open the link
/lightning/setup/FlexiPageList/home
. - Edit "Home Page Default", assign it as org default.
- Open org in Lightning, then open the link
There are some approaches came to me with 1 years of LWC components development. They are not obligatory to follow but they help me with everyday and can help you. They can and will be discussable.
In my life I saw some code in Angular and some code in React. And adding LWC to this list I found that in all the frameworks (I saw, but without a shadow of doubt, Vue supports the same opinion) there is a paradigm for using data:
The data of the component should be stored in the component's class and passed as properties to the child components.
You might ask "Where else can I store data?". Remember JQuery? The most basic use-case for it was capture input change and display it elsewhere.
$(".my-input").change(function () {
$(".my-label").text($(this).val());
});
Warning: I'm not a JQuery profi so the code might be better but I want to show you that you rely on the state of the different volatile tags' data.
- Single source of truth.
The total pain starts when you want to find the difference between the previous and the new values. Here you need to get the label text, but what if someone else changes it, even via debug console. And don't say "This is not JQuery, I can't even do this". Yes, you can. You can use
this.template.querySelector
and not store the data at all. But when you store all the data for your component in the class, you don't relay on any other source like child components or input values in the markup. - You don't depend on the re-render cycle.
Now when you pass the data from the class fields (optionally processed using getters) you don't need to track if your components retrieved with
this.template.querySelector
exist. LWC decides what to set during render flow. It simplifies the process of initializing the component.
Moreover, it makes your code simpler. Instead of manual putting the changes in any dependent component the changed values are put automatically with component re-render.
The home page shows data-storing-demo-container. It shows 3 components, focus on the first two of them for now.
Please see the data-storing-wrong-way and data-storing-right-way.
They implement the same functionality:
- There is a phone input with template
(3digits) 3digits-2digits-2digits [restDigits]
- The value is set from outside
- When focus, the editor sees only the numbers
- When the value is edited and focus is left, the template should be applied back.
The component "dataStoringWrongWay" passes got value straight to the input, what causes the issue of not existing lightning-input
component on initializing. Also, the code looks cleaner in "rightWay", isn't it?
With modern ES6 features JS becomes more able to use functional approach for the code we write. But together with functional approach you find that often functional approach articles talk about "immutable data". Of course your data does change, but the "immutable data" means that you must avoid the flows of changing the part of the data you have. It makes the code clear and LWC doesn't need to track the deep object changes. And during my development I found this strategy quite useful.
Every data change should
- Get required data
- Process it
- Assign the result to the class field
Previously re-assigning the field values was obligatory if you want to call re-render flow. After Salesforce version 48.0 (Spring'20) the deep changes can be tracked automatically. What you need to do is to add @track
decorator to the field. All fields without decorator are tracked for re-assigning the value, as it was before. That's why you can use data-changing methods (like array push
, pop
, splice
, object field assignment). But deep-tracked fields make more load on LWC and its proxies. Also it could be hard to find where your object or array was changed if you introduce a variable and pass it somewhere.
Suppose you have an Array of complex objects:
class A {
events = [
{
eventType: "webinar",
online: true,
offline: false,
participants: [{ firstName: "Andrey", lastName: "Ivanov" }]
}
];
onlineParticipants = [];
}
You need to find all online events and collect all the participants for the event.
The simple and maybe the faster way would be to use a for
fillOnlineParticipants() {
for(let i = 0; i < this.events.length; i++) {
if (this.events[i].online) {
for(let j = 0; j < this.events[i].participants; j++) {
this.onlineParticipants.push(this.events[i].participants[j]);
}
}
}
}
But this is a simple task. What if you need to collect more metrics with more sources? You will need more consequent and nested loops for it. What semi-functional approach can offer?
fillOnlineParticipants() {
this.onlineParticipants = getOnlineParticipants(this.events);
}
getOnlineParticipants(events) {
return events
.filter(_ => _.online)
.flatMap(_ => _.participants);
}
Oh, this was really shorter. The pros is code is simpler and it could be used somewhere else because it doesn't rely on this
. The cons here is the events
array is iterated twice. If you are a performance seeker, you can use reduce
and combine the methods in one.
getOnlineParticipants(events) {
return events.reduce((onlineParticipants, event) => {
if (event.online) {
return onlineParticipants.concat(event.participants);
}
return onlineParticipants;
}, []);
// or even
return events.reduce((onlineParticipants, event) => event.online
? onlineParticipants.concat(event.participants)
: onlineParticipants,
[]
);
}
Anyway this is only the example. What's more important to notice is not filter
, map
and reduce
functions, but the separate getOnlineParticipants
function that only accepts tha data and returns the data. In future if the bug happens, you will need to track what data was assigned to malformed fields and explore the functions that create this data. In the imperative way you might need to debug the functions step by step because it uses this
in many places. You of course can refactor from first way to the other but why don't you write the code the second way right away?
As it was mentioned previously, if it's possible, store the data in fields and use it in the template. But querySelector
is still a useful function. Let's see an example. You need to create an read/edit field with availability to cancel the changes. So you need to pass mode (view/edit) and data here. But do you need to keep the initial and changed data in the component? I don't think so. You are interested only in the result. That's why you can get the values only when you want to get them in the end of the editing the fields.
There you can make it work 2 ways:
- Fully aligned to value-event approach. When you change mode back to view, the input component throws a change event with old and new value and parent chooses what to store. Pros - event-driven, as Salesforce recommends. Cons - you need to handle what to store at the moment of event captured. It means that you need to store operation "save"/"cancel" during capturing all the events.
- Semi aligned to value-event approach. You pass the value straight via template. The process of the result:
a. "Edit" called - call component to store the initial value internally and
mode = "edit"
b. "Save" called - simply get the new value andmode = "view"
c. "Cancel" called - call component to restore the previous value andmode = "view"
This approach is implemented in data-passing-example Pros - you don't make the event handling logic. Cons - you make the logic in the parent component to save/rollback the value. In both ways you can affect the value of the child input if you change it in the parent (Click the "Refresh Components" in "c-data-storing-demo-container" when the "c-data-passing-example" is in edit mode, you will see the input value is changed).
Most of the JS code in LWC we write stays inside the class. This way class works as Aura controller and helper at once.
The example is import-example-container component which also utilizes import-example-utils as external code provider.
But some of the code can be stored outside the class. That's it, unlike the Aura components where you had only 2 objects here you can define fully separate function right in the same js file. The negative sign is these functions can be used only in the same file.
import { LightningElement } from "lwc";
/**
* Returns current timestamp as a string
* @returns {string}
*/
const getDateTime = () => {
return "" + new Date().getTime();
};
export default class ImportExampleContainer extends LightningElement {
someMethod() {
window.console.log(getDateTime());
}
}
So, shortly the properties:
purpose | availability scope | available in Aura | example |
---|---|---|---|
component-specific logic in small components | only in the file where declared | N/A | handleSameFileUpdateClick |
This way it is similar to Aura helpers, but here you can define as much separate JS files as you want. Their code can be available only inside the bundle.
This approach is very useful for component-specific data transforming functions if there is a lot of code. For instance, you can move util methods from the main JS file of import-example-container to importExampleContainerUtils.js. To allow to import functions separately you need to mark functions with export
statement. So now you can use it in main file after importing it from the file by relative path.
// importExampleContainerUtils.js
export const helpingFunction = (params) => {
// . . .
};
// importExampleContainer.js
import { helpingFunction } from "./importExampleContainerUtils";
// or, to rename the function in the destination file
import { helpingFunction as anotherNamedHelpingFunction } from "./importExampleContainerUtils";
// use here
If you want to export a single resource, like component-specific labels, you can export it as a default export source
// importExampleContainerLabels.js
// import labels here
export default { label1, label2, ... };
// importExampleContainer.js
import LABEL from "./importExampleContainerLabels";
// you can use any name instead of LABELS
// use here
Talking about multiple additional JS files, make sure there aren't cycle dependencies.
The properties:
name | purpose | availability scope | available in Aura | Example |
---|---|---|---|---|
export |
component-specific resources in components with a lot of logic | only in the bundle where declared - other JS files that are not imported in the source files, via import { ... } from "./..." |
N/A | handleAnotherFileUpdateClick |
export default |
component-specific resource if it is single to export in the file | only in the bundle where declared - other JS files that are not imported in the source files, via import ... from "./..." |
N/A | handleAnotherFileDefaultUpdateClick |
In fact, when you create a LWC component, LWC decides whether it's a component with the markup or just a bundle. To be a component that can be used in other components in markup, you need:
- your main (named the same way as the whole bundle) JS file should contain
export default
with type of class (in JS class is function tho, but let's skip this talk). - this class should extend
LightningElement
class imported from"lwc"
module. - js-meta.xml file is required any way.
And that's it. You can even remove HTML template at all if it is empty. See import-example-utils.
Another interesting fact is Local Dev Server understands that your bundle is a component by watching main JS file. If it imports LightningElement
- this is an element. Does it work or not is another question.
If you want to store the logic and use it in multiple components, you would rather create a bundle and export the resources from here. It's mostly like the previous approach, but there are some differences:
- You can import only the resources exported in the main JS file of the component
- You need to use the inter-component path. It's constructed from namespace and component name like "myNamespace/myComponent". In import-example-container you can see that it imports import-example-utils via
"c/importExampleUtils"
The same way as with files in the same component, you can also use export default
to export only one resource. As an example, you can define a bundle that imports the labels used by many components (buttons labels, toasts, etc)
// component uiLabels, uiLabels.js
export default {
buttonEdit,
buttonSave,
buttonCancel
};
// component that use ui labels, myComponent.js
import UI_LABEL from "c/uiLabels";
export default class MyComponent {
label = UI_LABEL;
}
<!-- myComponent.html -->
<lightning-button label="{label.buttonEdit}"></lightning-button>
Properties
name | purpose | availability scope | available in Aura | Example |
---|---|---|---|---|
export |
resources used in multiple components | in other bundles via import { ... } from "c/sharedResources" |
No | handleBundleUpdateClick |
export default |
single resource used by many components | in other bundles via import ... from "c/sharedResource" |
No | handleBundleDefaultUpdateClick |
This is a special use-case for the export logic that is needed to be used in the Aura components as well. This way create a component (needs to have an export default class extends LightningElement
) and link the resources you want to share with @api
decorated variables.
// availableAnywhere, availableAnywhere.js
import { LightningElement, api } from "lwc";
const publicMethod = (properties) => {
// ...
};
export default class AvailableAnywhere {
@api publicMethod(properties) {
return publicMethod(properties);
}
}
<!-- in LWC component template -->
<c-available-anywhere></c-available-anywhere>
// in LWC controller
callPublicMethod(properties) {
this.template.querySelector("c-available-anywhere")
.publicMethod(properties);
}
<!-- in Aura component template -->
<c:AvailableAnywhere aura:id="available-anywhere" />
// in Aura helper
callPublicMethod(component, properties) {
component.find("available-anywhere").publicMethod(properties);
}
Properties
purpose | availability scope | available in Aura | Example |
---|---|---|---|
resources available in LWC and Aura | in the components via adding the resource component ro the markup and call public methods on it. | Yes | handleComponentUpdateClick |
Another use-case for exports in a Lightning Component if you want to have this bundle as a component but store some additional resources to help with utilizing the component.
Let's see an example. You work on the custom table component which has a complex column settings and you want to share some simple functions to bootstrap the simple column settings creation.
There are 2 ways
-
export the functions together with default exported class. This way you will be able to both insert the component in the template and import the resources in controller.
// superTable, superTable.js export const createNumberColumnSetting = (fieldName) => { /* ... */ }; export const createStringColumnSetting = (fieldName) => { /* ... */ }; export const createDateColumnSetting = (fieldName) => { /* ... */ }; export default class SuperTable extends LightningElement { /* ... */ }
<!-- tableUsage, tableUsage.html --> <c-super-table columns={columns}," data={data}></c-super-table>
// tableUsage, tableUsage.js import { createNumberColumnSetting, createStringColumnSetting } from "c/superTable"; export default class TableUsage extends LightningElement { columns = [createNumberColumnSetting("id_num"), createStringColumnSetting("name")]; }
-
assign the shared resources to the static class fields. This way you will import component's class from default export and then get the resource you need as a static field/method.
// superTable, superTable.js export default class SuperTable extends LightningElement { static createNumberColumnSetting = (fieldName) => { /* ... */ }; static createStringColumnSetting = (fieldName) => { /* ... */ }; static createDateColumnSetting = (fieldName) => { /* ... */ }; }
<!-- tableUsage, tableUsage.html --> <c-super-table columns={columns}," data={data}></c-super-table>
// tableUsage, tableUsage.js import SuperTable from "c/superTable"; export default class TableUsage extends LightningElement { columns = [SuperTable.createNumberColumnSetting("id_num"), SuperTable.createStringColumnSetting("name")]; }
This was a topic I faced with and got into it only some months ago. But this thing sometimes is very helpful.
Sometimes you might face that there is a frequent mistake in simply accessing the field by the wrong name. For instance Apex returns Id
, LWC expects recordId
. How can you handle it for the complex objects? The super solution doesn't exist because javascript will always be a weak-type language and Salesforce is not going to allow us a simple way to add Typescript (MS is a competitor to SF, so SF have a reason for it).
Fortunately, what Salesforce offers us to use is VS Code. And this thing has an awesome implicit support for JSDoc which uses the same notation.
A simple use-case. You want to implement the picklist which uses "label"-"value" pair. But in some part of the code you forget if there should be "key", "name" or "value" field. The dought past away if you add the type to the argument you are working with.
/**
* Processes options
* @param {{label: string, value: string}[]} options options to process
*/
function processOptions(options) {}
Short clarifications:
- any type should be in
{}
, first comes field name, then it's type after a colon - JS supports it's primitives: number, string, boolean
- objects are declared as brackets with fields in it separated by a comma
- arrays are declared as
Array<TYPE>
orTYPE[]
, doesn't matter - if you want to create a type for Map-like structure which looks like an object, you can use
Object.<KEY_TYPE, VALUE_TYPE>
or{[KEY_NAME: KEY_TYPE]: VALUE_TYPE}
- If you want to specify a standard payload for custom event, set it as
CustomEvent<PAYLOAD_TYPE>
,event.detail
automatically will capture the type passed here. - functions can be declared as
(param1:PARAM_1_TYPE, ...) => RETURN_TYPE
or via @callback declaration separately
But repeating this notation with label and value is cumbersome. So that JSDOC has a @typedef declaration
/**
* @typedef {{label: string, value: string}} PicklistOption
*/
/**
* Processes options
* @param {PicklistOption[]} options options to process
*/
function processOptions(options) {}
After this declaration you can only mention PicklistOption
instead of the whole type.
What surprises me even now is types are implicitly exported from the file where you declare them. So that they can be used in other files/components.
The only limitation is you can not re-export the type. It means that you cannot define a type in a helper JS file, import it to the main one and then export it to import in another component.
Suppose we need a picklist but change
event needs to send {value, label}
pair. This way let's create a simple picklist component that sends custom "mychange" event with the whole option and track it in the main component. See the CustomPicklistChangeEvent
type in custom-picklist and its usage in custom-picklist-usage.
// myComponent.js
/**
* @typedef {...} MyType
*/
// ...
// myComponentUsage.js
import { MyType } from "c/myComponent";
A sad fact is SFDX plugin not always helps you with this. All the magic with "c/..."
imports is supported locally via lwc/jsconfig.json
file. To enable it, you need to have the next options. But during this project LWC did nothing to "compilerOptions"
property, it had only "experimentalDecorators"
and that's it. From the other hand, in another project it made all the paths automatically. That's why I added jsconfig to the repository despite it's ignored by default SFDX gitignore.
{
"compilerOptions": {
"baseUrl": ".",
"experimentalDecorators": true,
"target": "es6",
"paths": {
"c/componentName": ["componentName/componentName.js"]
}
}
}
Yes, they exist! And they are helpful in their field. They are stored in .sfdx/tools/typings/lwc
folder. The ones I liked are the types for UI API. There are plenty of properties on the fields and SObjects and to see them you either need to open the UI API documentation or perform a test launch and copy the whole object and store it internally. Honestly, I did it the second way. And once I was fed up opening it once again and created my own shorted type definitions. But during preparing the webinar material I found that they are already declared, for some reason - in "lightning/uiRecordApi"
module. But they work good! See the usage in the account-sobject-definition-fetcher
Unfortunately, you can't extend them because local .d.ts
files are only for your comfort. On Salesforce side all the imports are replaced with the links to their internal JS files.
- If the type is local only, you can declare it in a main JS file or in a separate helper if the type needs to be used in many internal JS files
- If the type is for multiple components that interact with your component (like
PicklistOption
in the example), define it in the component - If the type is for multiple components and there's no root component for it (in big code base there could be many components that use
PicklistOption
without referring to yourcustom-picklist
), then create a separate component (likecommonTypes
) and declare the type there - Your component classes don't need to have types by
@typedef
, you can export it and use as type in JSDoc comments
These topics are about the use-cases of LWC that are not well-known. Some of them you will never face, lucky you are. But forewarned is forearmed, so let's forearm you!
Since you started learning your first programming language, you must have heared the principle of dividing the functions (it even grew to first SOLID principle): each function should stand for one action. The same principle might must be applied to components: each component should implement one feature. And components lightning-formatted-something
align with this principle well. But lightning-input
doesn't: Salesforce needs a lot of internal components and code to display stylized date, time and color pickers. So despite it represents input in LWC environment, it's not fully one-purpose component. Imagine you need to do something similar. Your template will look like this.
<template>
<template if:true={firstVariant}>
<!-- much markup inside -->
</template>
<template if:true={secondVariant}>
<!-- even more markup inside -->
</template>
<!-- even more variants -->
</template>
Not the best look, is it? But Salesforce allows you to split the markup by a list of the templates and choose one of them during render flow.
<template>
<!-- first variant: much markup inside -->
</template>
<template>
<!-- second variant: much markup inside -->
<!-- but in another file -->
</template>
To do that create a render()
function. This function should return the markup. If it returns undefined
, no markup is rendered.
How to return the markup? Import it as "./myComponent.html"
file.
Let's view an example for Phone Input (data-passing-example) and split the view and Edit mode into two templates. Now open multi-template-example.
import templateEdit from "./multiTemplateExampleEdit.html";
import templateView from "./multiTemplateExampleView.html";
render() {
if (this.mode === "view") {
return templateView;
} else if (this.mode === "edit") {
return templateEdit;
}
return undefined;
}
What about CSS files? The rule is simple: your CSS file should have the same name as the template.
Can I transfer the template as a property from one component to another? Yes as well. Example: multi-template-example-dynamic. The real example of such necromancy is a Lightning Datatable with custom cell column types. You can define a class that extends LightningDatatable and add a static field with properties and references to your templates. Once created, your enhanced datatable will capture new columns and apply them where required.
Yes, it is possible as well. Since Summer'20 (v49.0) you can import stylesheets one into another. The example of internal imports in component is in multi-template-example. There is a common stylesheet "multiTemplateExampleCommon.css" which is imported by the others via
@import "./multiTemplateExampleCommon.css";
Don't forget the semicolon at the end!
The inter-component style is a little more difficult. Your component must have ONLY css
and js-meta.xml
files. The import process is the same to importing the default exported resource
@import "c/appBuilderStyled";
The examples of usage are all the displayed on the page components: data-storing-demo-container, custom-picklist-usage and the others. They all import app-builder-styled stylesheet. I just wanted them all to have a white background and a little padding for beauty. So this was a simple example of where to use it.
During the development I found an interesting issue with it: when you push the component which style imports stylesheet-component, the push fails if the stylesheet-component is not included. That's why during the examples development I had to change something in appBuilderStyled CSS to push that component and then change it back.
Have you ever wondered how standard accordion or tabset find their childs? Probably you thought about something like dynamic slot name, but unfortunately Salesforce doesn't support it. The real approach is not very interesting but it hides in knowledge about events. Event can be handled even on the component itself, this way it will listen to the whole markup inside.
// superTabset.js
connectedCallback() {
this.addEventListener("registersupertab", (event) => {
this.tabs = [...this.tabs, {
name: event.detail,
component: event.target
}];
})
}
Now your superTabs need to publish the registering event
// superTab.js
connectedCallback() {
this.dispatchEvent(new CustomEvent("registersupertab", detail: this.name));
}
@api show() { /* ... */}
@api hide() { /* ... */}
All the rest is to define the API to show or hide the tab. But when you want to define your tab behavior during show/hide actions there comes an unavailability to do it. There are 2 possible ways. First is store all this logic in the main component (the one that inserts tabset in your one).
The other is custom mixins
Talking about importing/exporting we exported and imported the functions to use them in the component logics. But those functions had one negative sign - they could not use this
object. And talking about unavailability for your own code, it makes the exported functions re-usable. But what if you want to have access to LightningElement
fields?
The easiest way is to pass this
as an argument to the functions.
Another more elegant way is mixins.
Mixin is a function that returns a class that extends a given one. You pass LightningElement
as given and extend it in your own component class. The result inheritance tree will be:
LightningElement
^
|
Mixed-in class
^
|
MyComponent
Personally I was tired writing this.template.querySelector
and this.dispatchEvent(new CustomEvent())
. I wanted some shorter function name, like this.$
and this.fire
. I could create these functions in every component or I could create a mixin or even mixed class.
// lightningCommons/lightningCommons.js
import { LightningElement as BaseLightningElement } from "lwc";
export const LightningBaseMixin = (baseClass) => {
return class extends baseClass {
$(selector) {
return this.template.querySelector(selector);
}
$$(selector) {
return this.template.querySelectorAll(selector);
}
fire(name, detail, bubbles = false, composed = false) {
this.dispatchEvent(new CustomEvent(name, { detail, bubbles, composed }));
}
};
};
export const LightningElement = LightningBaseMixin(LightningElementBase);
The limitation is you can't create component named lwc
and use api
, track
, wire
from any other module except lwc
, so full replacing "lwc"
by your component is impossible.
For the tabs example, we can define TabMixin
const TabMixin = (baseClass) =>
class extends baseClass {
constructor(...args) {
setTimeout(() => this.dispatchEvent(new CustomEvent("supertabregister")), 0);
}
};
Why not connectedCallback
- because you will need to write connectedCallback() { super.connectedCallback();}
. We don't want you to write extra boilerplate code.
The drawback is you need to define your own show
and hide
methods for not only your logic, but for showing/hiding as well. The cost of flexibility.
Exactly the same way famous NavigationMixin
works. The weird notation of this[NavigationMixin.Navigate]()
is only in order not to collide with your own methods.
There we should discuss what is the shadow root. The real shadow root encapsulates the parts of the dom, like a bounds for selectors and events. You can open lwc.dev recepies and see in the debug console that parts of the markup are separated by #shadow-root
element.
If you work with such components, the only way to style them from the outside is when the component allows you to do it adding part
attribute to it's markup.
<ui-header>
<!-- #shadow-root -->
<ul>
<li part="item">...</li>
</ul>
</ui-header>
/* outside code */
ui-header::part(item) {
/* styles applicable for shadowed component */
}
ui-header::part(item) .title {
/* styles NOT applicable for shadowed li
because going deep in the part is forbidden */
}
Salesforce LWC simulates shadow dom via adding the unique selectors in th markup to every tag of your template. So that your myComponent in the DOM will look like
<div c_my_component_my_component>
<span c_my_component_my_component>Hi</span>
<lightning-input c_my_component_my_component>
<input />
</lightning-input>
</div>
Meanwhile all your CSS written in the component is transformed to add the same unique attribute to all the parts of the selectors.
div[c_my_component_my_component] {
padding: 1em;
}
div[c_my_component_my_component] > span[c_my_component_my_component] {
font-weight: bold;
}
div[c_my_component_my_component] input[c_my_component_my_component] {
color: red; /* not applied, input has only lightning-input attr */
}
But static resource styles are not transformed at all. So that loading external css file is a common approach to change some parts of the standard markup. But make sure your style changes only your component, so that add a custom class to identify your components.
component - address-output