All skills
Skillintermediate
Lightning Web Components
Every Lightning Web Component consists of three files:
Claude Code Knowledge Pack7/10/2026
Overview
Lightning Web Components
Component Structure
Basic LWC Anatomy
Every Lightning Web Component consists of three files:
myComponent/
├── myComponent.html # Template
├── myComponent.js # JavaScript controller
└── myComponent.js-meta.xml # Configuration
HTML Template
<template>
<lightning-card title={cardTitle} icon-name="standard:account">
<div class="slds-p-around_medium">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading"></lightning-spinner>
</template>
<template lwc:else>
<template for:each={accounts} for:item="account">
<div key={account.Id} class="slds-m-bottom_small">
<lightning-tile
label={account.Name}
href={account.recordUrl}>
<p class="slds-truncate">{account.Industry}</p>
<p class="slds-text-body_small">
Revenue: <lightning-formatted-number
value={account.AnnualRevenue}
format-style="currency"
currency-code="USD">
</lightning-formatted-number>
</p>
</lightning-tile>
</div>
</template>
<template lwc:if={isEmpty}>
<div class="slds-align_absolute-center slds-p-around_large">
<p>No accounts found</p>
</div>
</template>
</template>
</div>
<div slot="footer">
<lightning-button
label="Refresh"
onclick={handleRefresh}
disabled={isLoading}>
</lightning-button>
</div>
</lightning-card>
</template>
JavaScript Controller
// accountCard.js
// Public properties - exposed to parent components
@api recordId;
@api maxRecords = 10;
// Private reactive properties
accounts = [];
error;
isLoading = true;
// Cached wire result for refresh
wiredAccountsResult;
// Computed properties
get cardTitle() {
return `Accounts (${this.accounts.length})`;
}
get isEmpty() {
return this.accounts.length === 0;
}
// Wire service - reactive data binding
@wire(getAccounts, { recordId: '$recordId', maxRecords: '$maxRecords' })
wiredAccounts(result) {
this.wiredAccountsResult = result;
const { data, error } = result;
if (data) {
this.accounts = data.map(account => ({
...account,
recordUrl: `/lightning/r/Account/${account.Id}/view`
}));
this.error = undefined;
} else if (error) {
this.error = error;
this.accounts = [];
this.showError('Error loading accounts', this.reduceErrors(error));
}
this.isLoading = false;
}
// Lifecycle hooks
connectedCallback() {
console.log('Component connected, recordId:', this.recordId);
}
disconnectedCallback() {
console.log('Component disconnected');
}
renderedCallback() {
// Called after every render
}
errorCallback(error, stack) {
console.error('Component error:', error, stack);
}
// Event handlers
handleRefresh() {
this.isLoading = true;
refreshApex(this.wiredAccountsResult)
.finally(() => {
this.isLoading = false;
});
}
// Helper methods
showError(title, message) {
this.dispatchEvent(new ShowToastEvent({
title,
message,
variant: 'error'
}));
}
reduceErrors(errors) {
if (!Array.isArray(errors)) {
errors = [errors];
}
return errors
.filter(error => !!error)
.map(error => {
if (typeof error === 'string') {
return error;
}
if (error.body?.message) {
return error.body.message;
}
if (error.message) {
return error.message;
}
return JSON.stringify(error);
})
.join(', ');
}
}
Meta Configuration
<?xml version="1.0" encoding="UTF-8"?>
<apiVersion>59.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Account Card</masterLabel>
<description>Displays account information in a card format</description>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
<target>lightningCommunity__Page</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Account</object>
<object>Contact</object>
</objects>
<property name="maxRecords" type="Integer" default="10"
label="Maximum Records" description="Max accounts to display" />
</targetConfig>
<targetConfig targets="lightning__AppPage,lightning__HomePage">
<property name="maxRecords" type="Integer" default="10"
label="Maximum Records" />
</targetConfig>
</targetConfigs>
Wire Service Patterns
Wire with Apex Methods
// Apex Controller
public with sharing class AccountController {
@AuraEnabled(cacheable=true)
public static List getAccounts(Id recordId, Integer maxRecords) {
return [
SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Id != :recordId
ORDER BY AnnualRevenue DESC NULLS LAST
LIMIT :maxRecords
];
}
@AuraEnabled
public static Account updateAccount(Id accountId, Map<String, Object> fields) {
Account acc = new Account(Id = accountId);
for (String fieldName : fields.keySet()) {
acc.put(fieldName, fields.get(fieldName));
}
update acc;
return acc;
}
}
// LWC using wire service
const FIELDS = [ACCOUNT_NAME, ACCOUNT_INDUSTRY, ACCOUNT_REVENUE];
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
account;
get accountName() {
return getFieldValue(this.account.data, ACCOUNT_NAME);
}
get industry() {
return getFieldValue(this.account.data, ACCOUNT_INDUSTRY);
}
async handleUpdate() {
const fields = {};
fields.Id = this.recordId;
fields[ACCOUNT_INDUSTRY.fieldApiName] = 'Technology';
try {
await updateRecord({ fields });
this.dispatchEvent(new ShowToastEvent({
title: 'Success',
message: 'Account updated',
variant: 'success'
}));
} catch (error) {
this.dispatchEvent(new ShowToastEvent({
title: 'Error',
message: error.body.message,
variant: 'error'
}));
}
}
}
Imperative Apex Calls
Use when you need control over when data is fetched.
searchTerm = '';
accounts = [];
isSearching = false;
handleSearchChange(event) {
this.searchTerm = event.target.value;
}
async handleSearch() {
if (this.searchTerm.length < 2) {
return;
}
this.isSearching = true;
try {
this.accounts = await searchAccounts({ searchTerm: this.searchTerm });
} catch (error) {
console.error('Search error:', error);
this.accounts = [];
} finally {
this.isSearching = false;
}
}
// Debounced search for better UX
debounceTimeout;
handleSearchInput(event) {
clearTimeout(this.debounceTimeout);
this.searchTerm = event.target.value;
this.debounceTimeout = setTimeout(() => {
this.handleSearch();
}, 300);
}
}
Component Communication
Parent to Child (Public Properties)
// Parent component
// parent.html
<template>
<c-child-component
account-id={selectedAccountId}
display-mode="compact"
onselect={handleChildSelect}>
</c-child-component>
</template>
// Child component
// childComponent.js
@api accountId;
@api displayMode = 'full'; // Default value
// Public method callable by parent
@api
refresh() {
// Refresh logic
}
@api
validate() {
const input = this.template.querySelector('lightning-input');
return input.reportValidity();
}
}
Child to Parent (Custom Events)
// Child component dispatching event
handleAccountSelect(event) {
const accountId = event.target.dataset.id;
// Simple event
this.dispatchEvent(new CustomEvent('select', {
detail: { accountId }
}));
// Bubbling event (crosses shadow DOM)
this.dispatchEvent(new CustomEvent('accountselected', {
detail: { accountId, accountName: this.accountName },
bubbles: true,
composed: true
}));
}
}
// Parent component handling event
// parent.html
<template>
<c-child-component onselect={handleSelect}></c-child-component>
</template>
// parent.js
handleSelect(event) {
const { accountId } = event.detail;
console.log('Selected account:', accountId);
}
Sibling Communication (Lightning Message Service)
// messageChannel/AccountSelected.messageChannel-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<masterLabel>Account Selected</masterLabel>
<isExposed>true</isExposed>
<description>Channel for account selection events</description>
<lightningMessageFields>
<fieldName>accountId</fieldName>
<description>Selected Account ID</description>
</lightningMessageFields>
<lightningMessageFields>
<fieldName>source</fieldName>
<description>Component that sent message</description>
</lightningMessageFields>
// Publisher component
@wire(MessageContext)
messageContext;
handleAccountClick(event) {
const accountId = event.target.dataset.id;
const payload = {
accountId: accountId,
source: 'AccountPublisher'
};
publish(this.messageContext, ACCOUNT_SELECTED, payload);
}
}
// Subscriber component
subscription = null;
selectedAccountId;
@wire(MessageContext)
messageContext;
connectedCallback() {
this.subscribeToMessageChannel();
}
disconnectedCallback() {
this.unsubscribeFromMessageChannel();
}
subscribeToMessageChannel() {
if (!this.subscription) {
this.subscription = subscribe(
this.messageContext,
ACCOUNT_SELECTED,
(message) => this.handleMessage(message)
);
}
}
unsubscribeFromMessageChannel() {
unsubscribe(this.subscription);
this.subscription = null;
}
handleMessage(message) {
this.selectedAccountId = message.accountId;
console.log('Received from:', message.source);
}
}
Form Handling
Lightning Record Edit Form
<template>
<lightning-record-edit-form
record-id={recordId}
object-api-name="Account"
onsuccess={handleSuccess}
onerror={handleError}
onsubmit={handleSubmit}>
<lightning-messages></lightning-messages>
<lightning-input-field field-name="Name"></lightning-input-field>
<lightning-input-field field-name="Industry"></lightning-input-field>
<lightning-input-field field-name="AnnualRevenue"></lightning-input-field>
<div class="slds-m-top_medium">
<lightning-button
type="submit"
variant="brand"
label="Save">
</lightning-button>
</div>
</lightning-record-edit-form>
</template>
@api recordId;
handleSubmit(event) {
event.preventDefault();
const fields = event.detail.fields;
// Modify fields before submission