Custom JavaScript

Custom JavaScript

Overview

Appify CPO lets you add custom JavaScript for advanced behavior beyond built-in configuration. There are two script types:

Script type When it runs Best for
Global script Once, automatically, when this calculator is ready (you do not listen for lifecycle events) Setup: analytics, DOM helpers, third-party widgets
Option script When that option’s value changes (and once on attach with the current value if the calculator already emitted) Reactive logic tied to a specific field

Configure them in Calculator Editor → Placement & Styling → JavaScript.

Custom scripts run in the storefront page context (not inside the calculator Shadow DOM). Use ctx.host / ctx.root to work inside the calculator instance.

Steps

  1. Open the JavaScript sub-tab in Placement & Styling.
  2. Global script — click Edit, write setup code using ctx, save.
  3. Option script — click Add option script, choose an option, write a handler that uses ctx, save.
  4. Publish and test on the storefront (and optionally enable Run custom JS in the admin preview).

Test thoroughly before publishing — script errors can break the shopper experience.

Global script

The editor prefills a short comment describing ctx. Your code runs once automatically when the calculator is ready for custom JS. You do not need:

document.addEventListener("calc:ready", ...);
document.addEventListener("calc:customJsReady", ...);

inside a Global script — Appify waits for the calculator, then runs your code.

Global ctx

Field Type Description
calculatorId string | null Calculator id
instanceId string | null Instance id (multi-calculator pages)
productId string | number | null Product id when linked to a product
host Element | null [data-calcai-calculator] host element
root ShadowRoot | Element | null Shadow root (or host) for DOM queries
setValue(optionId, value) function Set an option on this calculator via window.CalcAI

Global example

console.log("Calculator ready", ctx.calculatorId);

// Optional: keep listening for later shopper changes
document.addEventListener("calc:change", (e) => {
  const d = e.detail || {};
  if (d.calculatorId !== ctx.calculatorId) return;
  if (d.external) return;
  // d.values, d.totals, ...
});

For theme / third-party scripts (outside the editor)

After the Global script has run (or immediately if the Global script is empty), Appify dispatches:

document.addEventListener("calc:customJsReady", (e) => {
  const { calculatorId, instanceId, host, root } = e.detail || {};
  if (calculatorId !== "YOUR_CALC_ID") return;
  // Safe: calculator is rendered and calculator custom JS has finished startup
});

Helpers (when available):

  • window.CalcAI.isCustomJsReady("YOUR_CALC_ID")
  • window.CalcAI.getCustomJsReady("YOUR_CALC_ID") → last detail or null

Also see Storefront JavaScript API for window.CalcAI.setValue and calc:change payloads.

Option scripts and ctx

Option scripts are change handlers. The editor prefills a comment describing ctx. Your code runs inside a function that receives ctx whenever that option’s value changes, and once on attach with the current value if the calculator already emitted before the script loaded.

Does every option type use the same ctx shape?

Yes for the top-level keys. Every option script gets the same ctx fields (value, prevValue, values, totals, option, …).

What varies by option type is the type/shape of ctx.value and ctx.prevValue (and the matching entry inside ctx.values).

ctx reference

Field Type Description
value varies New value for this option
prevValue varies | undefined Previous value (undefined on the first run)
values object Map of all option keys → current values
totals object | null Pricing snapshot (see below)
summary object | null Price-summary payload used by summary blocks
option object { id, key, label, type } for this option
optionKey string This option’s key
calculatorId string | null Calculator id
instanceId string | null Instance id (multi-calculator pages)
productId string | number | null Product id when linked to a product
reason string | null e.g. "recompute", "draw", "external-set"
external boolean true if the change came from CalcAI.setValue / API
host Element | null [data-calcai-calculator] host element
root ShadowRoot | Element | null Shadow root (or host) for DOM queries inside the calculator
setValue(optionId, value) function Sets another option on this calculator via window.CalcAI

Value types (ctx.value / ctx.prevValue)

Option type Typical value
dropdown, select, radio, button, swatch, image-swatch String choice token
switch Boolean
number, slider, quantity Number
text, textarea, email, color, … String
checkbox-group / multi-select style Array of strings
dimensions, length, area, volume, weight Object

ctx.values uses the same per-key shapes for every option on the calculator.

Nested objects: how much detail?

We document stable top-level fields and the common nested keys merchants need. We do not freeze every nested property of totals / summary / full calculator spec in the editor comment — those can grow as pricing features evolve.

ctx.totals (common fields):

Field Meaning
subtotal Product line subtotal (before/with calculator discount rules as computed)
discountAmount Calculator discount amount
discountPercent Percent when applicable, else null
total Final calculator total (includes order fees when configured)
orderFees Array of { id, name, amount }
orderFeeTotal Sum of order fees
unitPrice Unit-price display info (value/raw/label/…) when available

ctx.values: plain object keyed by option key, e.g. { WIDTH: 48, COLOR: "red" }.

For storefront events (calc:ready, calc:change) and window.CalcAI.setValue, see Storefront JavaScript API.

Option script example

// Attached to option key WIDTH
if (ctx.external) return;

const n = Number(ctx.value);
if (!Number.isFinite(n)) return;

if (n > 96) {
  console.warn(ctx.option.label + " exceeds manufacturing max", n);
}

// Example: push a related option
// ctx.setValue("PRICE_TIER", n > 48 ? "large" : "standard");

Locating the calculator DOM from an option script

const host = ctx.host;                 // [data-calcai-calculator]
const root = ctx.root;                 // usually host.shadowRoot
const label = root?.querySelector?.(
  `#calc-${ctx.calculatorId}--label--${ctx.optionKey}`
);

Timing notes

  • Global scripts wait until the calculator is render-ready, then run once — even if the custom JS file loads after calc:ready.
  • Option scripts fire when calc:change reports a new value for that option key. If the script file loads after the calculator already emitted values, it runs once immediately with the current value (catch-up), then on later changes. Identical values are not re-fired.
  • Use ctx.external / reason === "external-set" to avoid feedback loops when you call ctx.setValue.
  • Theme scripts outside the calculator editor should use calc:customJsReady (or CalcAI.isCustomJsReady) instead of calc:ready if they depend on calculator custom JS having finished startup.