| name | sfcc-sfra-client-side-js |
| description | Guide for extending, structuring, validating, and optimizing client-side JavaScript in SFRA storefronts. Use when asked to build AJAX flows, form validation, DOM interactions, or client-side customizations. |
SFRA Client-Side JavaScript
Guide for building and extending client-side functionality in SFRA storefronts.
Quick Checklist
[ ] Use `paths` alias for base extension, never edit app_storefront_base
[ ] Register assets in main request context, not remote includes
[ ] Compose client overrides—no `module.superModule` (server-only)
[ ] Generate controller URLs server-side; pass via `data-*`
[ ] Always include CSRF token on POST (form.serialize() or manual)
[ ] Cache selectors & delegate events for dynamic regions
[ ] Batch DOM updates; debounce high-frequency handlers
[ ] Escape dynamic data inserted into DOM (XSS prevention)
[ ] Provide keyboard & screen reader accessible interactions
[ ] Namespace events for clean teardown
Architecture Overview
SFRA separates:
- Server runtime resolution (cartridge path in BM) – affects controllers, scripts, ISML
- Client build resolution (compile-time
paths aliases) – affects client/default/
Client behavior is determined at build time, not by the live cartridge path.
Directory Structure
app_custom_mybrand/
cartridge/
client/
default/
js/ # Feature modules
scss/ # Sass source
Guidelines:
- Never modify
app_storefront_base directly
- Mirror base file locations when extending
- Group by domain (
product/, checkout/)
Build Process
npm run compile:js
npm run compile:scss
Key distinction: Changing the BM cartridge path does not affect already compiled bundles. Recompile after structural changes.
Asset Loading
Register assets in ISML:
assets.addJs('/js/product/detail.js');
assets.addCss('/css/product.css');
Caveat: Assets added inside remote includes live only in that request and won't appear in the parent page.
Client-Side Extension
Extension uses build-time composition, not runtime inheritance.
1. Configure paths in package.json
{
"paths": {
"base": "../storefront-reference-architecture/cartridges/app_storefront_base/"
}
}
2. Require, Decorate, Export
'use strict';
var base = require('base/product/detail');
base.updateAddToCartButton = function (update) {
$('button.add-to-cart').attr('disabled', !update.readyToOrder);
};
base.initNotifyMe = function () {
$('body').on('click', '.notify-me', handleNotifyMe);
};
module.exports = base;
Note: module.superModule does NOT exist client-side.
jQuery Best Practices
Selector Caching
var $price = $('.product-price');
$price.text('$99.99');
$price.addClass('updated');
Event Delegation
$('.product-grid').on('click', '.add-to-cart', handler);
Batch DOM Writes
var html = products.map(p => `<li>${escapeHtml(p.name)}</li>`).join('');
$('#product-list').html(html);
AJAX Pattern
$('#newsletter-form').on('submit', function (e) {
e.preventDefault();
var $form = $(this);
$form.spinner().start();
$.ajax({
url: $form.data('action-url'),
type: 'post',
data: $form.serialize(),
success: function (data) {
$form.spinner().stop();
showMessage(data.success ? 'success' : 'danger', data.message);
},
error: function (err) {
$form.spinner().stop();
var msg = (err.responseJSON && err.responseJSON.message) || 'Error occurred';
showMessage('danger', msg);
}
});
});
Performance
| Technique | Benefit |
|---|
| Debounce typing | Reduces AJAX bursts (250-400ms) |
| Cache selectors | Lower DOM traversal cost |
| Build HTML in memory | Fewer reflows |
| Event delegation | Handles dynamic content |
function debounce(fn, wait) {
var t;
return function () {
clearTimeout(t);
var args = arguments, ctx = this;
t = setTimeout(function () { fn.apply(ctx, args); }, wait);
};
}
$('#search').on('input', debounce(fetchSuggestions, 300));
Accessibility
- Use semantic elements (
<button>, <nav>)
- Add
aria-live="polite" to AJAX-updated regions
- Manage focus on modal open/close
- Maintain keyboard operability
- Reflect state with ARIA (
aria-expanded, aria-pressed)
$('.filter-toggle').on('click', function () {
var $panel = $('#filter-panel');
var expanded = $(this).attr('aria-expanded') === 'true';
$(this).attr('aria-expanded', !expanded);
$panel.toggleClass('is-open', !expanded);
if (!expanded) {
$panel.find('input,button,a').first().focus();
}
});
Security: XSS Prevention
Always escape dynamic data:
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
$('<div>').text(userInput);
Never trust: query params, form inputs, server-returned user data.
Module Design Pattern
'use strict';
var selectors = { container: '.favorites-container' };
function bindEvents($root) {
$root.on('click.favoritesAdd', '.favorite-add', onAdd);
}
function unbindEvents($root) {
$root.off('.favoritesAdd');
}
module.exports = {
init: function () { bindEvents($(selectors.container)); },
destroy: function () { unbindEvents($(selectors.container)); }
};
Guidelines:
- Namespace every delegated event (
.featureAction)
- Export
init() + optional destroy()
- Keep pure logic in separate files for testing
Form Validation Flow
User Input → HTML5 check → (fail) prevent + style
→ (pass) AJAX submit → Server re-validate → Success OR Error JSON
→ Error: map messages → show per-field + global feedback
Always validate both client (UX) and server (authority).
Internationalization
- Never concatenate partial Resource strings
- Use
Resource.msgf with placeholders
- Avoid hard-coded punctuation in code
var template = $('#banner').data('welcome-template');
$('#banner .text').text(template.replace('{name}', firstName));
Testing Strategy
- Pure utilities: Jest unit tests (no DOM)
- DOM behavior: jsdom + jQuery
- Contract tests: Ensure extended module exports required API
- A11y smoke: Test live regions, keyboard access
Detailed References