Appearance
Dealer experience plugin
This example builds a standalone dealer experience that highlights a preferred brand, adds a help banner, opens a shopping modal, forwards analytics data, and presents curated vehicle builds.
It uses native browser APIs, so its only required browser dependency is the public plugin SDK.
Project structure
text
dealer-plugin/
├── package.json
├── tsconfig.json
├── vite.config.ts
└── src/
├── plugin.ts
└── preboot.tsInstall the versioned SDK package supplied for plugin development:
bash
npm install ./iconfigurators-plugin-sdk-VERSION.tgz
npm install --save-dev typescript viteDefine deployment options
Options keep customer-specific labels and identifiers outside the reusable plugin definition.
ts
import {
PLUGIN_API_VERSION,
definePlugin,
type PluginBrandRecord,
} from '@iconfigurators/plugin-sdk'
export type DealerExperienceOptions = {
dealerName?: string
featuredBrandSlug?: string
badgeLabel?: string
bannerTitle?: string
bannerBody?: string
}
type DealerBrand = PluginBrandRecord & {
dealerFeatured?: boolean
}Mark the featured brand
Use brands:filter to return decorated copies of the existing records. The extra field is then available to the typed brand-card outlet.
ts
api.hooks.filter('brands:filter', (brands, context) => {
if (context.source !== 'brands') return brands
return brands.map<DealerBrand>((brand) => ({
...brand,
dealerFeatured: brand.slug?.toLowerCase() === options.featuredBrandSlug.toLowerCase(),
}))
})
api.ui.mount('brands:card:badges', (container, context) => {
const brand = context.brand as DealerBrand
if (!brand.dealerFeatured) return
const badge = document.createElement('span')
badge.className = 'dealer-experience-badge'
badge.textContent = options.badgeLabel
container.appendChild(badge)
return () => badge.remove()
})Return a new array and record rather than changing the received brand objects.
Forward an event to the host page
The plugin can translate a configurator event into a customer-owned browser event. Send only the fields the host integration needs.
ts
api.events.on('brands:loaded', (event) => {
const featuredBrand = event.data.brands.find(
(brand) => brand.slug?.toLowerCase() === options.featuredBrandSlug.toLowerCase(),
)
window.dispatchEvent(
new CustomEvent('dealer-experience:brands-loaded', {
detail: {
featuredBrandId: featuredBrand?.id ?? null,
resultCount: event.data.resultCount,
},
}),
)
})The embedding website can listen without depending on the plugin SDK:
js
window.addEventListener('dealer-experience:brands-loaded', (event) => {
console.log(event.detail.featuredBrandId, event.detail.resultCount)
})Add a dealer-help modal
Mount a banner into brands:top and use api.ui.openModal() for the workflow. Icon Visualizer owns the dialog shell while the plugin owns the modal body.
ts
api.ui.mount('brands:top', (container) => {
const banner = document.createElement('aside')
const title = document.createElement('strong')
const button = document.createElement('button')
banner.className = 'dealer-experience-banner'
title.textContent = options.bannerTitle
button.type = 'button'
button.textContent = 'Get shopping help'
banner.append(title, button)
let closeModal: (() => void) | undefined
const showHelp = () => {
closeModal?.()
closeModal = api.ui.openModal({
id: 'dealer-experience-help',
title: `Shop with ${options.dealerName}`,
description: 'Choose how you want to begin.',
size: 'medium',
render(modalContainer, context) {
const chooseVehicle = document.createElement('button')
chooseVehicle.type = 'button'
chooseVehicle.textContent = 'Shop by vehicle'
const openSearch = () => {
api.commands.openSearchByVehicle()
context.close('api')
}
chooseVehicle.addEventListener('click', openSearch)
modalContainer.appendChild(chooseVehicle)
return () => {
chooseVehicle.removeEventListener('click', openSearch)
chooseVehicle.remove()
}
},
})
}
button.addEventListener('click', showHelp)
container.appendChild(banner)
return () => {
closeModal?.()
button.removeEventListener('click', showHelp)
banner.remove()
}
})Add curated vehicle builds
Register a complete landing section when the plugin owns content larger than a small outlet contribution. runAction() executes a documented action and records the item selection.
ts
api.ui.registerLandingSection({
id: 'customer-builds',
placement: 'between-featured-brands-and-new-arrivals',
order: 10,
title: 'Customer builds',
description: 'Explore vehicle and wheel combinations selected by the dealer team.',
render(container, context) {
const list = document.createElement('ul')
const builds = [
{
id: 'FEATURED_BUILD_ID',
title: 'Featured vehicle build',
query: {
fmk: 'VEHICLE_FMK',
bodyType: 'BODY_TYPE_ID',
sizeCategory: 'OE',
wheel: 'WHEEL_IMAGE_ID',
wheelDiameter: 'WHEEL_DIAMETER',
},
},
]
for (const build of builds) {
const item = document.createElement('li')
const button = document.createElement('button')
button.type = 'button'
button.textContent = build.title
button.addEventListener('click', () => {
void context.runAction(build.id, {
command: 'openVisualizer',
query: build.query,
})
})
item.appendChild(button)
list.appendChild(item)
}
container.appendChild(list)
return () => list.remove()
},
})For a production carousel, retain each listener cleanup and remove them before removing the section content.
Register styles
Register styles with the plugin API so they apply to plugin UI inside the configurator. Prefix classes with the plugin name to prevent collisions.
ts
api.ui.addStyle(`
.dealer-experience-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid #cbd5e1;
border-radius: 0.5rem;
}
.dealer-experience-badge {
padding: 0.25rem 0.5rem;
border-radius: 999px;
background: #2563eb;
color: white;
font-weight: 700;
}
`)Assemble the plugin
Place the registrations inside one plugin definition. Default options make missing optional values predictable.
ts
export default definePlugin<DealerExperienceOptions>({
id: 'customer.dealer-experience',
version: '1.0.0',
apiVersion: PLUGIN_API_VERSION,
setup(api, configuredOptions) {
const options = {
dealerName: 'Dealer',
featuredBrandSlug: 'FEATURED_BRAND_SLUG',
badgeLabel: 'Dealer pick',
bannerTitle: 'Need help choosing a wheel?',
bannerBody: 'A specialist can help you compare compatible options.',
...configuredOptions,
}
// Register the hook, events, UI, and styles shown above here.
},
})Queue customer-specific options
The preboot entry supplies deployment-specific values without changing the reusable plugin.
ts
// src/preboot.ts
import { queuePlugin } from '@iconfigurators/plugin-sdk'
import dealerExperiencePlugin from './plugin'
queuePlugin(dealerExperiencePlugin, {
order: 20,
required: false,
options: {
dealerName: 'YOUR_DEALER_NAME',
featuredBrandSlug: 'FEATURED_BRAND_SLUG',
badgeLabel: 'Recommended',
bannerTitle: 'Need help choosing the right wheel?',
bannerBody: 'A specialist can help you compare compatible options.',
},
})Build the browser artifact
ts
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: 'src/preboot.ts',
name: 'DealerExperiencePlugin',
formats: ['iife'],
fileName: () => 'dealer-experience.plugin.js',
},
},
})The output must include the SDK helpers and all plugin-owned dependencies.
Load and verify
html
<script src="https://YOUR_DOMAIN/plugins/dealer-experience.plugin.js"></script>
<div id="icf_page"></div>
<script src="https://iconfigurators.app/src/embed.cfm?ky=CONFIGURATOR_KEY"></script>js
document.addEventListener('iconfigurator.ready', () => {
const registration = window.iConfigurator.getPluginStatus('customer.dealer-experience')
console.assert(registration?.status === 'ready')
})Continue with Test a plugin and Release and rollback before enabling the plugin on a production page.