Configuring JS layout
Fast Simon Shopper Assistant uses advanced view rendering to bring the most optimized user experience. To adjust elements or logic we provide unique hooks you can use to edit the elements we generate.
Getting Started
Shopper Assistant hook Flow
- Register a hook using a script tag located in the
<head> - Shopper Assistant will pickup the hook and will execute any code you write there
- Hook will run again on every life cycle update
Rules
- Registering multiple hooks with the same event will execute all registered callbacks in order.
- You should only manipulate the dom element provided by the hook.
- We only update the dom nodes that changed in each life cycle change, so make sure your code doesn't add the same elements twice (see examples)
- Hook runs per component, data will contain the specific component state
Register a new hook & execute it
<script>
function shopperAssistantHooks() {
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.ChatHeader,
({element, title, subtitle, config}) => {
// Customize chat header
element.style.borderBottom = '2px solid gold';
}
);
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.Message,
({element, content, messageType, products}) => {
// Customize individual messages
if (messageType === 'system') {
element.style.backgroundColor = '#f0f0f0';
}
}
);
}
// execution here
if (window.FastSimonChat) {
shopperAssistantHooks();
} else {
window.addEventListener('fast-chat-assistant-ready', function () {
shopperAssistantHooks();
});
}
</script>
Delete a registered hook
<script>
// To delete a hook, pass null as the callback
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.ChatHeader,
null
);
</script>
Available Hooks
| Event Name | Description | Hook Params | Return Params |
|---|---|---|---|
| "chat-header" | Life cycle hook: every chat header update | object: {element: HTMLElement, title: string, subtitle: string, config: ChatConfig} | void |
| "chat-footer" | Life cycle hook: every chat footer update | object: {element: HTMLElement, currentInput: string, isMessageLoading: boolean} | void |
| "chat-body" | Life cycle hook: every messages container update | object: {element: HTMLElement, messages: Message[], isMessageLoading: boolean} | void |
| "message" | Life cycle hook: every individual message update | object: {element: HTMLElement, content: string, type: MessageType, messageType: 'user' | 'system', products?: ServerProduct[], messageId?: string} | void |
type references: Typescript Types
Hook Patterns
Working with Shadow DOM
The Shopper Assistant uses Shadow DOM in production builds. When customizing elements, make sure to:
- Use the provided
elementparameter to access the DOM - Avoid using
document.querySelector()- useelement.querySelector()orelement.querySelectorAll()instead - Styles added via hooks must be inline or injected into the shadow root
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.ChatBody,
({element, messages}) => {
// ✅ Correct: Use the provided element to query all message containers
const messageContainers = element.querySelectorAll('.messages-container > *');
messageContainers.forEach((container, index) => {
// Add message number badge
if (!container.querySelector('.message-number')) {
const badge = document.createElement('span');
badge.className = 'message-number';
badge.textContent = `#${index + 1}`;
badge.style.cssText = 'position: absolute; top: 5px; right: 5px; background: #ccc; padding: 2px 6px; font-size: 10px; border-radius: 10px;';
container.style.position = 'relative';
container.appendChild(badge);
}
});
// ❌ Incorrect: Don't use document directly
// const messageContainer = document.querySelector('.messages-container');
}
);
Avoiding Duplicate Elements
Since hooks run on every lifecycle update, ensure you don't add duplicate elements:
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.ChatHeader,
({element}) => {
// Remove existing custom element first
const existing = element.querySelector('.my-custom-badge');
if (existing) {
existing.remove();
}
// Now add the new element
const badge = document.createElement('div');
badge.className = 'my-custom-badge';
badge.textContent = 'VIP Support';
element.appendChild(badge);
}
);
Manipulating Product Prices in Carousel
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.Message,
({element, messageType, products}) => {
if (messageType === 'system' && products && products.length > 0) {
// Query all product carousel items
const productItems = element.querySelectorAll('.chat-product-carousel-item-container');
productItems.forEach((productItem, index) => {
// Find the price container
const priceContainer = productItem.querySelector('.product-price-container');
const priceElement = productItem.querySelector('.product-price');
if (priceContainer && priceElement) {
// Remove existing discount badge
const existingBadge = priceContainer.querySelector('.discount-badge');
if (existingBadge) {
existingBadge.remove();
}
// Get product data
const product = products[index];
if (product) {
const price = parseFloat(product.p);
const comparePrice = parseFloat(product.p_c);
// Calculate and show discount percentage
if (comparePrice > price) {
const discount = Math.round(((comparePrice - price) / comparePrice) * 100);
const badge = document.createElement('div');
badge.className = 'discount-badge';
badge.textContent = `-${discount}%`;
badge.style.cssText = 'background: #FF4444; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; margin-left: 8px;';
priceContainer.appendChild(badge);
}
}
}
});
}
}
);
Conditional Logic Based on Message Type
window.FastSimonChat.registerHook(
window.FastSimonChat.CustomizationEvent.Message,
({element, messageType, products, content}) => {
if (messageType === 'system' && products && products.length > 0) {
// Add a custom badge for product recommendations
if (!element.querySelector('.products-badge')) {
const badge = document.createElement('span');
badge.className = 'products-badge';
badge.textContent = `${products.length} product${products.length > 1 ? 's' : ''} recommended`;
badge.style.cssText = 'background: #4CAF50; color: white; padding: 4px 10px; border-radius: 4px; font-size: 12px; display: inline-block; margin-bottom: 8px; font-weight: 500;';
element.prepend(badge);
}
}
if (messageType === 'user') {
// Customize user messages
element.style.backgroundColor = '#e3f2fd';
element.style.borderRadius = '12px';
element.style.padding = '10px 14px';
}
}
);