sfb2b-lwc

star 0

LWC development best practices for B2B Commerce storefront including component architecture, templates, styling, state management, testing, performance optimization, and common pitfalls for {{clientName}}

Architect-And-Bot By Architect-And-Bot schedule Updated 2/12/2026

name: sfb2b-lwc description: LWC development best practices for B2B Commerce storefront including component architecture, templates, styling, state management, testing, performance optimization, and common pitfalls for {{clientName}}

B2B Commerce LWC Development Guide

Component Architecture

B2B Commerce Component Hierarchy

Standard Structure

{{projectPrefixKebab}}-storefront (top-level page wrapper)
├── {{projectPrefixKebab}}-header (site header)
│   ├── {{projectPrefixKebab}}-search-bar
│   ├── {{projectPrefixKebab}}-nav-menu
│   └── {{projectPrefixKebab}}-cart-icon
├── {{projectPrefixKebab}}-main-content
│   ├── {{projectPrefixKebab}}-sidebar (category filters or nav)
│   └── {{projectPrefixKebab}}-page-content
│       ├── {{projectPrefixKebab}}-product-listing (PCP)
│       ├── {{projectPrefixKebab}}-product-detail (PDP)
│       ├── {{projectPrefixKebab}}-checkout-flow
│       └── {{projectPrefixKebab}}-account-page
└── {{projectPrefixKebab}}-footer

Component Communication Pattern

  • Parent -> Child: properties (passed as @api decorated properties)
  • Child -> Parent: events (dispatch custom events)
  • Sibling communication: pub-sub (Lightning Message Service or custom event bus)
  • Shared state: data service (singleton Apex controller or LWC data adapter)

Slot-Based Composition

Benefit: Flexible template composition without hardcoding child components

Example: Product Card with Slots

<!-- {{projectPrefixKebab}}-product-card.html -->
<template>
  <div class="product-card">
    <slot name="image"></slot>
    <slot name="title"></slot>
    <slot name="price"></slot>
    <slot name="actions"></slot>
  </div>
</template>

Usage in Parent

<!-- {{projectPrefixKebab}}-product-listing.html -->
<template for:each="{products}" for:item="product">
  <{{projectPrefixKebab}}-product-card key="{product.id}" product="{product}">
    <{{projectPrefixKebab}}-product-image slot="image" product="{product}"></{{projectPrefixKebab}}-product-image>
    <{{projectPrefixKebab}}-product-title slot="title" product="{product}"></{{projectPrefixKebab}}-product-title>
    <{{projectPrefixKebab}}-product-price slot="price" product="{product}"></{{projectPrefixKebab}}-product-price>
    <{{projectPrefixKebab}}-add-to-cart-button slot="actions" product="{product}"></{{projectPrefixKebab}}-add-to-cart-button>
  </{{projectPrefixKebab}}-product-card>
</template>

Data Service Pattern: Wire vs Imperative

Wire Adapter Pattern (Reactive)

  • Automatic refresh when inputs change
  • Best for: static queries, stable dependencies
  • Example: @wire(getProduct, { productId: '$productId' })
  • Limitation: watch all inputs, not selective refresh

Imperative Pattern (Explicit Control)

  • Call service manually, handle refresh logic
  • Best for: user-triggered actions, complex logic, performance optimization
  • Example: click button -> call service -> update state
  • Advantage: fine-grained control, can skip updates

Decision Matrix

Use Wire When               | Use Imperative When
- Page loads, show data    | - User triggers action
- Input stable, no change  | - Conditional fetching
- Simple presentation      | - Complex error handling
                            | - Performance: skip updates

Example: Product Detail with Hybrid Approach

// Wire: auto-load product definition
@wire(getProduct, { productId: '$productId' })
product;

// Imperative: call pricing on demand
handleLoadPricing() {
  getPricing({ productId: this.productId })
    .then(pricing => this.pricing = pricing)
    .catch(error => this.showError(error));
}

Event Bubbling and Pub-Sub

Event Bubbling

  • Child dispatch custom event -> propagates up to parent
  • Use for close parent-child relationships
  • Example: cart line item delete button -> dispatch "itemdeleted" event -> cart listener removes item

Pub-Sub (Lightning Message Service)

  • Decoupled communication between unrelated components
  • Publish event -> all subscribers notified
  • Best for: app-wide state changes (cart updated, user logged in)
  • Avoid: tight coupling, overuse (can cause performance issues)

Example: Cart Update Notification

// Component A: delete cart item
import { publish, MessageContext } from 'lightning/messageService';
import CART_UPDATED from '@salesforce/messageChannel/CartUpdated__c';

deleteItem() {
  // Delete logic...
  publish(this.messageContext, CART_UPDATED, { cartId: this.cartId });
}

// Component B: listen for cart updates
import { subscribe, MessageContext } from 'lightning/messageService';
import CART_UPDATED from '@salesforce/messageChannel/CartUpdated__c';

connectedCallback() {
  this.subscription = subscribe(this.messageContext, CART_UPDATED, (message) => {
    this.refreshCart(message.cartId);
  });
}

B2B Commerce Components

Extending Standard Commerce Components

Product Card Component

Standard component provides: image, title, price display Extension opportunities: add custom badges, promotions, quick-view modal

// {{projectPrefixKebab}}-product-card.js
import { LightningElement, api } from 'lwc';

export default class {{projectPrefixPascal}}ProductCard extends LightningElement {
  @api product;
  @api showPromoBadge = true;

  get isPromoProduct() {
    return this.product.isOnPromotion;
  }

  handleQuickView() {
    this.dispatchEvent(
      new CustomEvent('quickview', {
        detail: { productId: this.product.id },
      }),
    );
  }
}

Cart Component

Standard component provides: line item list, update quantity, remove item Extension opportunities: add discount code field, shipping estimate, save for later

// {{projectPrefixKebab}}-cart-summary.js
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import CART_FIELDS from '@salesforce/apex/CartController.getCartFields';

export default class {{projectPrefixPascal}}CartSummary extends LightningElement {
  @api cartId;

  @wire(getRecord, { recordId: '$cartId', fields: CART_FIELDS })
  cartData;

  get total() {
    return this.cartData?.data?.fields?.TotalAmount?.value ?? 0;
  }

  get taxAmount() {
    return this.cartData?.data?.fields?.TaxAmount?.value ?? 0;
  }
}

Checkout Component

Standard component provides: step navigation, form validation Extension opportunities: custom approval step, delivery location selector, payment integration


Component Templates

Product Detail Page Component

// {{projectPrefixKebab}}-product-detail.js
import { LightningElement, api, wire, track } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import getPricingInfo from '@salesforce/apex/PricingController.getPricing';
import getProductImages from '@salesforce/apex/ProductController.getImages';

const PRODUCT_FIELDS = [
  'Product2.Id',
  'Product2.Name',
  'Product2.Description',
  'Product2.ProductCode',
  'Product2.StockKeepingUnit',
];

export default class {{projectPrefixPascal}}ProductDetail extends LightningElement {
  @api productId;
  @track quantity = 1;
  @track selectedVariant = null;
  @track pricingInfo;
  @track productImages;
  isLoading = false;
  errorMessage;

  @wire(getRecord, { recordId: '$productId', fields: PRODUCT_FIELDS })
  product;

  @wire(getPricingInfo, { productId: '$productId' })
  wiredPricing({ error, data }) {
    if (data) {
      this.pricingInfo = data;
      this.errorMessage = null;
    } else if (error) {
      this.errorMessage = 'Unable to load pricing information';
    }
  }

  connectedCallback() {
    this.loadProductImages();
  }

  loadProductImages() {
    getProductImages({ productId: this.productId })
      .then((images) => {
        this.productImages = images;
      })
      .catch((error) => {
        console.error('Error loading images:', error);
      });
  }

  handleQuantityChange(event) {
    this.quantity = parseInt(event.target.value, 10);
  }

  handleVariantSelect(event) {
    this.selectedVariant = event.detail;
  }

  async handleAddToCart() {
    this.isLoading = true;
    try {
      const result = await addProductToCart({
        productId: this.productId,
        quantity: this.quantity,
        variantId: this.selectedVariant?.id,
      });
      this.dispatchEvent(
        new CustomEvent('cartitemadded', {
          detail: result,
        }),
      );
    } catch (error) {
      this.errorMessage = error.message;
    } finally {
      this.isLoading = false;
    }
  }

  get unitPrice() {
    return this.pricingInfo?.unitPrice ?? 0;
  }

  get totalPrice() {
    return this.unitPrice * this.quantity;
  }

  get isOutOfStock() {
    return this.pricingInfo?.availableQuantity === 0;
  }
}
<!-- {{projectPrefixKebab}}-product-detail.html -->
<template>
  <div class="product-detail">
    <template if:true="{isLoading}">
      <lightning-spinner></lightning-spinner>
    </template>

    <template if:false="{isLoading}">
      <div class="detail-container">
        <!-- Product Images -->
        <div class="images-section">
          <{{projectPrefixKebab}}-product-gallery images="{productImages}"></{{projectPrefixKebab}}-product-gallery>
        </div>

        <!-- Product Info -->
        <div class="info-section">
          <h1>{product.fields.Name.value}</h1>
          <p class="sku">SKU: {product.fields.ProductCode.value}</p>

          <!-- Pricing -->
          <div class="pricing-section">
            <span class="unit-price">${unitPrice}</span>
            <span class="total-price">Total: ${totalPrice}</span>
          </div>

          <!-- Variant Selection (if applicable) -->
          <template if:true="{product.fields.HasVariants}">
            <{{projectPrefixKebab}}-variant-selector
              onvariantselect="{handleVariantSelect}"
            ></{{projectPrefixKebab}}-variant-selector>
          </template>

          <!-- Quantity -->
          <div class="quantity-section">
            <label for="qty">Quantity:</label>
            <input
              id="qty"
              type="number"
              value="{quantity}"
              onchange="{handleQuantityChange}"
              min="1"
            />
          </div>

          <!-- Add to Cart -->
          <button class="add-to-cart-btn" onclick="{handleAddToCart}" disabled="{isOutOfStock}">
            {isOutOfStock ? 'Out of Stock' : 'Add to Cart'}
          </button>

          <!-- Error Message -->
          <template if:true="{errorMessage}">
            <div class="error-message">{errorMessage}</div>
          </template>

          <!-- Product Description -->
          <div class="description-section">
            <h3>Description</h3>
            <p>{product.fields.Description.value}</p>
          </div>
        </div>
      </div>
    </template>
  </div>
</template>

Cart Line Item Component

// {{projectPrefixKebab}}-cart-line-item.js
import { LightningElement, api, track } from 'lwc';

export default class {{projectPrefixPascal}}CartLineItem extends LightningElement {
  @api cartItem;
  @api editable = true;
  @track quantity;

  connectedCallback() {
    this.quantity = this.cartItem.quantity;
  }

  handleQuantityChange(event) {
    this.quantity = parseInt(event.target.value, 10);
    this.dispatchEvent(
      new CustomEvent('quantitychange', {
        detail: {
          cartItemId: this.cartItem.id,
          quantity: this.quantity,
        },
      }),
    );
  }

  handleRemoveItem() {
    this.dispatchEvent(
      new CustomEvent('removeitem', {
        detail: { cartItemId: this.cartItem.id },
      }),
    );
  }

  get subtotal() {
    return this.cartItem.unitPrice * this.quantity;
  }

  get discountedPrice() {
    if (this.cartItem.discount > 0) {
      return this.cartItem.unitPrice - this.cartItem.discount;
    }
    return this.cartItem.unitPrice;
  }
}
<!-- {{projectPrefixKebab}}-cart-line-item.html -->
<template>
  <div class="cart-line-item">
    <div class="item-image">
      <img src="{cartItem.productImage}" alt="{cartItem.productName}" />
    </div>

    <div class="item-details">
      <h4>{cartItem.productName}</h4>
      <p class="sku">SKU: {cartItem.sku}</p>
      <p class="price">${cartItem.unitPrice}</p>
    </div>

    <div class="item-quantity">
      <label>Qty:</label>
      <input
        type="number"
        value="{quantity}"
        onchange="{handleQuantityChange}"
        disabled="{!editable}"
        min="1"
      />
    </div>

    <div class="item-total">
      <p>${subtotal}</p>
    </div>

    <template if:true="{editable}">
      <button class="remove-btn" onclick="{handleRemoveItem}">Remove</button>
    </template>
  </div>
</template>

Checkout Step Component

// {{projectPrefixKebab}}-checkout-step.js
import { LightningElement, api, track } from 'lwc';

export default class {{projectPrefixPascal}}CheckoutStep extends LightningElement {
  @api stepNumber;
  @api stepTitle;
  @api isActive = false;
  @api isCompleted = false;
  @track formData = {};
  @track errors = [];

  handleInputChange(event) {
    const { name, value } = event.target;
    this.formData[name] = value;
  }

  async handleStepComplete() {
    this.errors = [];

    try {
      // Validate form
      const isValid = this.validate();
      if (!isValid) {
        return;
      }

      // Emit complete event
      this.dispatchEvent(
        new CustomEvent('stepcomplete', {
          detail: {
            stepNumber: this.stepNumber,
            formData: this.formData,
          },
          bubbles: true,
          composed: true,
        }),
      );
    } catch (error) {
      this.errors.push(error.message);
    }
  }

  validate() {
    // Step-specific validation logic
    return true;
  }

  handleStepBack() {
    this.dispatchEvent(
      new CustomEvent('stepback', {
        detail: { stepNumber: this.stepNumber },
        bubbles: true,
        composed: true,
      }),
    );
  }
}
<!-- {{projectPrefixKebab}}-checkout-step.html -->
<template>
  <div class="checkout-step">
    <div class="step-header">
      <span class="step-number">{stepNumber}</span>
      <h3>{stepTitle}</h3>
    </div>

    <template if:true="{isActive}">
      <div class="step-content">
        <slot></slot>
      </div>

      <template if:true="{errors}">
        <div class="errors">
          <template for:each="{errors}" for:item="error">
            <p key="{error}" class="error-message">{error}</p>
          </template>
        </div>
      </template>

      <div class="step-actions">
        <button onclick="{handleStepBack}" class="btn-back">Back</button>
        <button onclick="{handleStepComplete}" class="btn-next">Next</button>
      </div>
    </template>

    <template if:false="{isActive}">
      <div class="step-status">
        <template if:true="{isCompleted}">
          <span class="status-completed">Completed</span>
        </template>
        <template if:false="{isCompleted}">
          <span class="status-pending">Pending</span>
        </template>
      </div>
    </template>
  </div>
</template>

My Account Section Component

// {{projectPrefixKebab}}-account-section.js
import { LightningElement, api, wire, track } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import getCurrentUser from '@salesforce/apex/AccountController.getCurrentUser';
import getAccountOrders from '@salesforce/apex/OrderController.getAccountOrders';

export default class {{projectPrefixPascal}}AccountSection extends LightningElement {
  @api activeTab = 'profile';
  @track userProfile;
  @track orders;
  @track addresses;
  isLoading = false;

  @wire(getCurrentUser)
  wiredUser({ error, data }) {
    if (data) {
      this.userProfile = data;
    } else if (error) {
      console.error('Error loading user:', error);
    }
  }

  @wire(getAccountOrders)
  wiredOrders({ error, data }) {
    if (data) {
      this.orders = data;
    }
  }

  handleTabChange(event) {
    this.activeTab = event.detail.tab;
  }

  async handleProfileUpdate(event) {
    this.isLoading = true;
    try {
      await updateUserProfile({ userData: event.detail });
      this.dispatchEvent(new CustomEvent('profileupdated'));
    } catch (error) {
      console.error('Error updating profile:', error);
    } finally {
      this.isLoading = false;
    }
  }
}
<!-- {{projectPrefixKebab}}-account-section.html -->
<template>
  <div class="account-section">
    <{{projectPrefixKebab}}-tab-group onchangetab={handleTabChange}>
      <{{projectPrefixKebab}}-tab label="Profile" name="profile">
        <template if:true={activeTab.startsWith('profile')}>
          <{{projectPrefixKebab}}-account-profile
            user={userProfile}
            onprofileupdate={handleProfileUpdate}
          ></{{projectPrefixKebab}}-account-profile>
        </template>
      </{{projectPrefixKebab}}-tab>

      <{{projectPrefixKebab}}-tab label="Order History" name="orders">
        <template if:true={activeTab.startsWith('orders')}>
          <{{projectPrefixKebab}}-order-history orders={orders}></{{projectPrefixKebab}}-order-history>
        </template>
      </{{projectPrefixKebab}}-tab>

      <{{projectPrefixKebab}}-tab label="Addresses" name="addresses">
        <template if:true={activeTab.startsWith('addresses')}>
          <{{projectPrefixKebab}}-address-list addresses={addresses}></{{projectPrefixKebab}}-address-list>
        </template>
      </{{projectPrefixKebab}}-tab>
    </{{projectPrefixKebab}}-tab-group>
  </div>
</template>

Search Filter Component

// {{projectPrefixKebab}}-search-filter.js
import { LightningElement, api, track, wire } from 'lwc';
import getFilterOptions from '@salesforce/apex/SearchController.getFilterOptions';

export default class {{projectPrefixPascal}}SearchFilter extends LightningElement {
  @api searchQuery;
  @track selectedFilters = {};
  @track filterOptions;
  @track isExpanded = false;

  @wire(getFilterOptions, { searchQuery: '$searchQuery' })
  wiredFilters({ error, data }) {
    if (data) {
      this.filterOptions = data;
    }
  }

  handleFilterChange(event) {
    const { filterName, value, checked } = event.detail;

    if (!this.selectedFilters[filterName]) {
      this.selectedFilters[filterName] = [];
    }

    if (checked) {
      this.selectedFilters[filterName].push(value);
    } else {
      this.selectedFilters[filterName] = this.selectedFilters[filterName].filter(
        (v) => v !== value,
      );
    }

    this.dispatchEvent(
      new CustomEvent('filterchange', {
        detail: this.selectedFilters,
        bubbles: true,
      }),
    );
  }

  handleClearFilters() {
    this.selectedFilters = {};
    this.dispatchEvent(
      new CustomEvent('filterclear', {
        bubbles: true,
      }),
    );
  }

  toggleExpand() {
    this.isExpanded = !this.isExpanded;
  }
}
<!-- {{projectPrefixKebab}}-search-filter.html -->
<template>
  <div class="search-filter">
    <button class="filter-toggle" onclick="{toggleExpand}">
      Filters <span class="toggle-icon">{isExpanded ? '−' : '+'}</span>
    </button>

    <template if:true="{isExpanded}">
      <div class="filter-panel">
        <template for:each="{filterOptions}" for:item="filter">
          <div key="{filter.name}" class="filter-group">
            <h4>{filter.label}</h4>
            <template for:each="{filter.options}" for:item="option">
              <label key="{option.value}">
                <input
                  type="checkbox"
                  onchange="{handleFilterChange}"
                  data-filter-name="{filter.name}"
                  data-value="{option.value}"
                />
                {option.label} ({option.count})
              </label>
            </template>
          </div>
        </template>

        <button class="btn-clear" onclick="{handleClearFilters}">Clear Filters</button>
      </div>
    </template>
  </div>
</template>

Styling

CSS Custom Properties for Theming

Define Variables in Root

/* {{projectPrefixKebab}}-theme.css */
:root {
  --color-primary: #0070d2;
  --color-secondary: #005a9c;
  --color-success: #04844b;
  --color-error: #d0342d;
  --color-warning: #ff9300;
  --color-text: #333;
  --color-border: #d8dce6;
  --color-bg-light: #f7f8fc;

  --font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-sm: 0.875rem;

  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-base: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;

  --border-radius: 0.25rem;
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
  --shadow-md: 0 3px 6px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.12);
}

/* Use in components */
.button {
  background-color: var(--color-primary);
  color: white;
  padding: var(--spacing-base) var(--spacing-lg);
  border-radius: var(--border-radius);
  font-family: var(--font-family-sans);
  box-shadow: var(--shadow-sm);
}

.button:hover {
  background-color: var(--color-secondary);
  box-shadow: var(--shadow-md);
}

SLDS (Salesforce Lightning Design System) Usage

Standard Component Classes

<template>
  <!-- Button styles -->
  <button class="slds-button slds-button_brand">Primary Button</button>

  <!-- Card component -->
  <div class="slds-card">
    <div class="slds-card__header slds-grid">
      <header class="slds-media slds-media_center slds-has-flexi-truncate">
        <div class="slds-media__body">
          <h2 class="slds-card__title">Card Title</h2>
        </div>
      </header>
    </div>
    <div class="slds-card__body">Card content</div>
  </div>

  <!-- Form elements -->
  <div class="slds-form-element">
    <label class="slds-form-element__label">Label</label>
    <div class="slds-form-element__control">
      <input type="text" class="slds-input" />
    </div>
  </div>

  <!-- Grid layout -->
  <div class="slds-grid slds-wrap slds-grid_pull-padded">
    <div class="slds-col slds-size_1-of-2 slds-p-horizontal_small">Item 1</div>
    <div class="slds-col slds-size_1-of-2 slds-p-horizontal_small">Item 2</div>
  </div>
</template>

Responsive Design Patterns

Mobile-First Approach

/* Mobile (default) */
.product-grid {
  display: grid;
  grid-template-columns: 1fr; /* 1 column on mobile */
  gap: 1rem;
}

.sidebar {
  display: none; /* Hide sidebar on mobile */
}

/* Tablet: 768px and up */
@media (min-width: 768px) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr); /* 2 columns */
  }

  .sidebar {
    display: block;
  }
}

/* Desktop: 1024px and up */
@media (min-width: 1024px) {
  .product-grid {
    grid-template-columns: repeat(3, 1fr); /* 3 columns */
  }
}

Accessibility

ARIA Labels and Roles

<template>
  <!-- Button with aria-label for icon-only buttons -->
  <button aria-label="Close modal" class="close-btn">
    <svg class="icon"><!-- close icon --></svg>
  </button>

  <!-- Navigation with proper ARIA -->
  <nav aria-label="Product Categories">
    <ul class="category-list">
      <li><a href="/category/dept-a">Department A</a></li>
      <li><a href="/category/dept-b">Department B</a></li>
    </ul>
  </nav>

  <!-- Form validation with aria-describedby -->
  <div class="slds-form-element slds-has-error">
    <label class="slds-form-element__label">Email</label>
    <div class="slds-form-element__control">
      <input type="email" aria-describedby="email-error" required />
    </div>
    <span id="email-error" class="slds-form-element__help"> Invalid email format </span>
  </div>

  <!-- Skip to main content link -->
  <a href="#main-content" class="skip-link">Skip to main content</a>
</template>

Keyboard Navigation

// Ensure interactive elements are keyboard accessible
handleKeydown(event) {
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    this.handleClick();
  }
}

// Modal trap focus within modal
connectedCallback() {
  this.focusableElements = this.template.querySelectorAll(
    'button, [href], input, [tabindex]:not([tabindex="-1"])'
  );
  this.firstElement = this.focusableElements[0];
  this.lastElement = this.focusableElements[this.focusableElements.length - 1];
}

handleModalKeydown(event) {
  if (event.key === 'Tab') {
    if (event.shiftKey && document.activeElement === this.firstElement) {
      event.preventDefault();
      this.lastElement.focus();
    } else if (!event.shiftKey && document.activeElement === this.lastElement) {
      event.preventDefault();
      this.firstElement.focus();
    }
  }
}

State Management

Wire Service for Commerce Data

Basic Wire Adapter

import { wire } from 'lwc';
import getProducts from '@salesforce/apex/ProductController.getProducts';

export default class {{projectPrefixPascal}}ProductListing extends LightningElement {
  @wire(getProducts, { categoryId: '$categoryId' })
  products;

  get isLoading() {
    return this.products.data === undefined && this.products.error === undefined;
  }

  get errorMessage() {
    return this.products.error?.body?.message;
  }
}

Cart State Management

Shared Cart Service

// {{projectPrefixKebab}}-cart-service.js
export class CartService {
  constructor() {
    this.cartId = null;
    this.items = [];
    this.listeners = [];
  }

  setCart(cartId) {
    this.cartId = cartId;
    this.notifyListeners();
  }

  addItem(item) {
    this.items.push(item);
    this.notifyListeners();
  }

  removeItem(itemId) {
    this.items = this.items.filter((item) => item.id !== itemId);
    this.notifyListeners();
  }

  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  }

  notifyListeners() {
    this.listeners.forEach((listener) => listener(this.getState()));
  }

  getState() {
    return {
      cartId: this.cartId,
      items: this.items,
      total: this.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    };
  }
}

// Singleton instance
export const cartService = new CartService();

Using Cart Service in Components

import { LightningElement, track } from 'lwc';
import { cartService } from './{{projectPrefixKebab}}-cart-service';

export default class {{projectPrefixPascal}}CartIcon extends LightningElement {
  @track cartTotal = 0;

  connectedCallback() {
    this.unsubscribe = cartService.subscribe((state) => {
      this.cartTotal = state.items.length;
    });
  }

  disconnectedCallback() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }
}

NavigationMixin for Page Navigation

import { LightningElement, api } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';

export default class {{projectPrefixPascal}}ProductCard extends NavigationMixin(LightningElement) {
  @api product;

  handleProductClick() {
    // Navigate to product detail page
    this[NavigationMixin.Navigate]({
      type: 'standard__webPage',
      attributes: {
        url: `/product/${this.product.id}`,
      },
    });
  }

  handleCartClick() {
    // Navigate to cart
    this[NavigationMixin.Navigate]({
      type: 'standard__webPage',
      attributes: {
        url: '/cart',
      },
    });
  }
}

Lightning/uiRecordApi Patterns

import { LightningElement, api, wire } from 'lwc';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import ID_FIELD from '@salesforce/schema/Account.Id';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import EMAIL_FIELD from '@salesforce/schema/Account.Email__c';

export default class {{projectPrefixPascal}}AccountEditor extends LightningElement {
  @api recordId;
  @track formData = {};

  @wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, EMAIL_FIELD] })
  account;

  handleInputChange(event) {
    const { name, value } = event.target;
    this.formData = { ...this.formData, [name]: value };
  }

  async handleSave() {
    try {
      await updateRecord({
        fields: {
          Id: this.recordId,
          Name: this.formData.name,
          Email__c: this.formData.email,
        },
      });
      this.dispatchEvent(new CustomEvent('recordupdated'));
    } catch (error) {
      console.error('Error updating record:', error);
    }
  }
}

Testing

Jest Test Patterns for LWC

Basic Component Test

// {{projectPrefixKebab}}-product-card.test.js
import { createElement } from 'lwc';
import {{projectPrefixPascal}}ProductCard from 'c/{{projectPrefixLower}}ProductCard';

describe('{{projectPrefixKebab}}-product-card', () => {
  afterEach(() => {
    // Clean up
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it('renders product name', () => {
    const element = createElement('{{projectPrefixKebab}}-product-card', {
      is: {{projectPrefixPascal}}ProductCard,
    });
    element.product = {
      id: '123',
      name: 'Test Product',
      price: 9.99,
    };
    document.body.appendChild(element);

    const heading = element.shadowRoot.querySelector('h3');
    expect(heading.textContent).toBe('Test Product');
  });

  it('dispatches addtocart event', () => {
    const element = createElement('{{projectPrefixKebab}}-product-card', {
      is: {{projectPrefixPascal}}ProductCard,
    });
    element.product = { id: '123', name: 'Product' };
    document.body.appendChild(element);

    const handler = jest.fn();
    element.addEventListener('addtocart', handler);

    const button = element.shadowRoot.querySelector('.add-btn');
    button.click();

    expect(handler).toHaveBeenCalled();
  });
});

Mocking Wire Adapters

import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
import getProduct from '@salesforce/apex/ProductController.getProduct';

const getProductAdapter = registerApexTestWireAdapter(getProduct);

describe('{{projectPrefixKebab}}-product-detail', () => {
  it('displays product from wire adapter', async () => {
    const mockProduct = {
      id: '123',
      name: 'Test Product',
      price: 9.99,
    };

    const element = createElement('{{projectPrefixKebab}}-product-detail', {
      is: {{projectPrefixPascal}}ProductDetail,
    });
    element.productId = '123';
    document.body.appendChild(element);

    // Emit data from wire adapter
    getProductAdapter.emit(mockProduct);

    await element.updateComplete;

    const heading = element.shadowRoot.querySelector('h1');
    expect(heading.textContent).toContain('Test Product');
  });
});

Mocking Apex Calls

import { createElement } from 'lwc';
import {{projectPrefixPascal}}Cart from 'c/{{projectPrefixLower}}Cart';

// Mock the Apex method
jest.mock(
  '@salesforce/apex/CartController.addToCart',
  () => ({
    default: jest.fn(),
  }),
  { virtual: true },
);

import addToCart from '@salesforce/apex/CartController.addToCart';

describe('{{projectPrefixKebab}}-cart', () => {
  it('calls addToCart on button click', async () => {
    addToCart.mockResolvedValue({ success: true });

    const element = createElement('{{projectPrefixKebab}}-cart', {
      is: {{projectPrefixPascal}}Cart,
    });
    document.body.appendChild(element);

    const button = element.shadowRoot.querySelector('.add-btn');
    button.click();

    await element.updateComplete;

    expect(addToCart).toHaveBeenCalled();
  });

  it('handles Apex error', async () => {
    const error = new Error('Cart update failed');
    addToCart.mockRejectedValue(error);

    const element = createElement('{{projectPrefixKebab}}-cart', {
      is: {{projectPrefixPascal}}Cart,
    });
    document.body.appendChild(element);

    const button = element.shadowRoot.querySelector('.add-btn');
    button.click();

    await element.updateComplete;

    const errorMsg = element.shadowRoot.querySelector('.error');
    expect(errorMsg).toBeTruthy();
  });
});

DOM Assertions

describe('DOM assertions', () => {
  it('renders list items', () => {
    const element = createElement('{{projectPrefixKebab}}-product-list', {
      is: {{projectPrefixPascal}}ProductList,
    });
    element.products = [
      { id: '1', name: 'Product 1' },
      { id: '2', name: 'Product 2' },
    ];
    document.body.appendChild(element);

    const items = element.shadowRoot.querySelectorAll('.product-item');
    expect(items.length).toBe(2);
    expect(items[0].textContent).toContain('Product 1');
  });

  it('shows/hides elements conditionally', () => {
    const element = createElement('{{projectPrefixKebab}}-loading', {
      is: {{projectPrefixPascal}}Loading,
    });
    element.isLoading = true;
    document.body.appendChild(element);

    let spinner = element.shadowRoot.querySelector('.spinner');
    expect(spinner).toBeTruthy();

    element.isLoading = false;
    return element.updateComplete.then(() => {
      spinner = element.shadowRoot.querySelector('.spinner');
      expect(spinner).toBeFalsy();
    });
  });
});

Event Testing

describe('event testing', () => {
  it('dispatches custom event with detail', () => {
    const element = createElement('{{projectPrefixKebab}}-product-card', {
      is: {{projectPrefixPascal}}ProductCard,
    });
    element.product = { id: '123' };
    document.body.appendChild(element);

    const handler = jest.fn();
    element.addEventListener('quickview', handler);

    const button = element.shadowRoot.querySelector('.quick-view-btn');
    button.click();

    expect(handler).toHaveBeenCalledWith(
      expect.objectContaining({
        detail: expect.objectContaining({
          productId: '123',
        }),
      }),
    );
  });
});

Performance

Lazy Loading

// Lazy load images in product grid
export default class {{projectPrefixPascal}}ProductListing extends LightningElement {
  renderedCallback() {
    const images = this.template.querySelectorAll('img');

    if ('IntersectionObserver' in window) {
      const imageObserver = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            imageObserver.unobserve(img);
          }
        });
      });

      images.forEach((img) => imageObserver.observe(img));
    }
  }
}

Virtual Scrolling for Large Lists

// Virtual scrolling for 200+ items
import { LightningElement, api, track } from 'lwc';

export default class {{projectPrefixPascal}}VirtualList extends LightningElement {
  @api items;
  @api itemHeight = 60;
  @track visibleItems = [];
  @track startIndex = 0;

  connectedCallback() {
    this.updateVisibleItems();
  }

  handleScroll(event) {
    const container = event.target;
    const scrollTop = container.scrollTop;

    this.startIndex = Math.floor(scrollTop / this.itemHeight);
    this.updateVisibleItems();
  }

  updateVisibleItems() {
    const visibleCount = Math.ceil(500 / this.itemHeight) + 2; // Buffer
    this.visibleItems = this.items.slice(this.startIndex, this.startIndex + visibleCount);
  }

  get containerHeight() {
    return 500; // px
  }

  get offsetY() {
    return this.startIndex * this.itemHeight;
  }
}

Image Optimization

<!-- Use responsive images and formats -->
<template>
  <picture>
    <source srcset="image.webp" type="image/webp" />
    <source srcset="image.jpg" type="image/jpeg" />
    <img src="image.jpg" alt="Product image" loading="lazy" width="300" height="300" />
  </picture>
</template>

Bundle Size Awareness

Code Splitting

// Dynamic import for heavy components
async loadAdvancedFeatures() {
  const { {{projectPrefixPascal}}AdvancedSearch } = await import(
    /* webpackChunkName: "advanced" */
    './{{projectPrefixKebab}}-advanced-search'
  );
  // Use component
}

Common Pitfalls

Locker Service Limitations

Limitation: Cannot access window globals directly

// WRONG
const xhr = new XMLHttpRequest(); // Locker Service blocks direct access

// CORRECT
import { callApex } from '@salesforce/apex/Controller.method';
// Use Apex callout instead

Limitation: Cross-origin iframe restrictions

// Cannot embed arbitrary external iframes
<template>
  <iframe src="https://external-site.com"></iframe>
</template>

// Use Salesforce-hosted content or visual force iframe
<lightning-iframe
  src={iframeUrl}
  title="External Content"
></lightning-iframe>

Shadow DOM Gotchas

Challenge: Styling from outside component doesn't affect shadow DOM

/* {{projectPrefixKebab}}-product-card.css - scoped to component */
:host {
  /* Style the component root */
  display: block;
  padding: 1rem;
}

:host(.highlighted) {
  /* Support external styling via class */
  background-color: yellow;
}

Challenge: querySelector doesn't work across shadow boundaries

// Can't find element inside shadow DOM
document.querySelector('.product-card button');

// Use component method or expose via @api
@api getButton() {
  return this.template.querySelector('button');
}

B2B Commerce API Quirks

Quirk: Product.StockKeepingUnit is read-only

// Cannot update SKU
update({ fields: { Id, StockKeepingUnit } });

// SKU is set at product creation

Quirk: CartCalculator runs on every cart change

// Must be performant and idempotent
handleCartCalculation() {
  // Avoid expensive operations or side effects
  // Called frequently during session
}

Pagination Patterns

// Implement offset-based pagination
export default class {{projectPrefixPascal}}SearchResults extends LightningElement {
  @track results = [];
  @track currentPage = 1;
  @track pageSize = 20;
  @track totalRecords = 0;

  handlePageChange(event) {
    this.currentPage = event.detail.page;
    this.loadResults();
  }

  async loadResults() {
    const offset = (this.currentPage - 1) * this.pageSize;
    const response = await searchProducts({
      query: this.searchQuery,
      offset,
      limit: this.pageSize,
    });

    this.results = response.records;
    this.totalRecords = response.total;
  }

  get totalPages() {
    return Math.ceil(this.totalRecords / this.pageSize);
  }
}

Component Naming Convention

All components prefixed with {{projectPrefixKebab}}- for {{clientName}} branding:

  • {{projectPrefixKebab}}-product-card -- Product tile in listing
  • {{projectPrefixKebab}}-product-detail -- Full product detail page
  • {{projectPrefixKebab}}-cart-summary -- Cart overview
  • {{projectPrefixKebab}}-cart-line-item -- Single cart item
  • {{projectPrefixKebab}}-checkout-step -- Checkout flow step
  • {{projectPrefixKebab}}-search-bar -- Search input
  • {{projectPrefixKebab}}-search-filter -- Filter sidebar
  • {{projectPrefixKebab}}-category-nav -- Category navigation
  • {{projectPrefixKebab}}-account-section -- User account area
  • {{projectPrefixKebab}}-order-history -- Order list
  • {{projectPrefixKebab}}-payment-form -- Payment entry

Related Resources

See also:

Install via CLI
npx skills add https://github.com/Architect-And-Bot/sf-b2b-commerce-template --skill sfb2b-lwc
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
Architect-And-Bot
Architect-And-Bot Explore all skills →