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