Skip to content

Lit promotion plugin

This example renders a stateful promotion with lit-html. Lit belongs to the plugin and is bundled into its browser artifact. Icon Visualizer supplies the outlet container and public commands but does not supply a renderer.

Use this approach only when a plugin owns enough dynamic UI to justify the additional dependency.

Install dependencies

bash
npm install ./iconfigurators-plugin-sdk-VERSION.tgz lit-html
npm install --save-dev typescript vite

Define the plugin

The render function reads plugin-owned state. Clicking the button redraws the contribution and then opens the vehicle search through a supported command.

ts
// src/plugin.ts
import { html, nothing, render } from 'lit-html'
import { PLUGIN_API_VERSION, definePlugin } from '@iconfigurators/plugin-sdk'

export type PromotionOptions = {
  title?: string
  body?: string
  actionLabel?: string
}

const styles = `
  .customer-promotion {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.75rem 1rem;
    padding: 1rem;
    border: 1px solid #cbd5e1;
    border-radius: 0.5rem;
  }

  .customer-promotion__status {
    grid-column: 1 / -1;
  }

  @media (max-width: 640px) {
    .customer-promotion {
      grid-template-columns: 1fr;
    }
  }
`

export default definePlugin<PromotionOptions>({
  id: 'customer.vehicle-search-promotion',
  version: '1.0.0',
  apiVersion: PLUGIN_API_VERSION,

  setup(api, configuredOptions) {
    const options = {
      title: 'Find wheels made for your vehicle',
      body: 'Start with compatible results and compare your options.',
      actionLabel: 'Choose my vehicle',
      ...configuredOptions,
    }

    api.ui.addStyle(styles)

    api.ui.mount('brands:top', (container) => {
      let searchOpenCount = 0

      const openVehicleSearch = () => {
        searchOpenCount += 1
        draw()
        api.commands.openSearchByVehicle()
      }

      const view = () => html`
        <aside class="customer-promotion">
          <div>
            <strong>${options.title}</strong>
            <p>${options.body}</p>
          </div>
          <button type="button" @click=${openVehicleSearch}>${options.actionLabel}</button>
          <small class="customer-promotion__status" aria-live="polite">
            ${searchOpenCount === 0
              ? 'Vehicle search has not been opened.'
              : `Vehicle search opened ${searchOpenCount} time${searchOpenCount === 1 ? '' : 's'}.`}
          </small>
        </aside>
      `

      const draw = () => render(view(), container)
      draw()

      return () => render(nothing, container)
    })
  },
})

Why cleanup matters

The outlet may mount more than once as users navigate. Calling render(nothing, container) in the returned cleanup disposes Lit event bindings and clears the rendered content. Do not retain the outlet container after cleanup.

Resources registered through api.ui.addStyle() and api.ui.mount() are also removed when the plugin is unregistered.

Queue the plugin

ts
// src/preboot.ts
import { queuePlugin } from '@iconfigurators/plugin-sdk'
import promotionPlugin from './plugin'

queuePlugin(promotionPlugin, {
  order: 30,
  required: false,
  options: {
    title: 'Find your next wheel package',
    body: 'Begin with your vehicle for compatible results.',
    actionLabel: 'Shop by vehicle',
  },
})

Bundle Lit with the plugin

ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/preboot.ts',
      name: 'VehicleSearchPromotionPlugin',
      formats: ['iife'],
      fileName: () => 'vehicle-search-promotion.plugin.js',
    },
  },
})

Do not mark lit-html as external. The resulting file must be able to run without a host-provided renderer.

Load and verify

html
<script src="https://YOUR_DOMAIN/plugins/vehicle-search-promotion.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.vehicle-search-promotion')
  console.assert(registration?.status === 'ready')
})

Compare this approach with the dealer experience plugin, which provides similar UI without a renderer dependency.

Icon Visualizer developer documentation